EstevezAlvarez
Microservicios Arquitectura Python

Microservicios: arquitectura escalable y la importancia de modelar bien

Dividir una aplicación en microservicios es sencillo. Dividirla bien requiere entender qué es un dominio, por qué cada servicio debe ser dueño de sus datos, y cuándo no dividir es la decisión correcta.

Monolito Módulo Usuarios Módulo Pedidos Módulo Pagos Módulo Catálogo una sola base de datos Microservicios Svc Usuarios DB: users_db Svc Pedidos DB: orders_db Svc Pagos DB: payments_db Svc Catálogo DB: catalog_db API Gateway
En el monolito todos los módulos comparten base de datos y se despliegan juntos. En microservicios cada servicio es dueño de sus datos y se despliega de forma independiente.

Del monolito al microservicio

Un monolito no es malo. Para equipos pequeños y productos en fase temprana, un monolito bien estructurado es más barato de mantener que una malla de servicios. El problema aparece cuando el sistema crece: un cambio en el módulo de pagos requiere redesplegar toda la aplicación; escalar el catálogo de productos significa escalar también usuarios y pedidos aunque no lo necesiten; el equipo de frontend no puede avanzar porque el backend tiene una deuda técnica acumulada en un solo repositorio gigante.

Microservicios son la solución a ese problema específico — no a todos los problemas. La decisión de migrar tiene sentido cuando los cuellos de botella de despliegue, escalado o autonomía de equipo justifican la complejidad operativa añadida.

Por qué el modelo de datos es la decisión más importante

Muchos equipos comienzan dividiendo el código en servicios pero manteniendo una base de datos compartida. Es el peor de los dos mundos: tienes la complejidad de red de los microservicios sin la independencia de despliegue del monolito.

La regla es simple pero difícil de respetar: cada servicio es el único dueño de sus datos. Ningún otro servicio puede leer o escribir directamente en su base de datos. Si el Servicio de Pedidos necesita el nombre del cliente, lo pide al Servicio de Usuarios mediante una API — no hace un JOIN cross-service.

Esto tiene consecuencias en el diseño. Los datos que antes vivían en una tabla ahora tienen que existir en múltiples servicios en sus respectivos contextos. El usuario para el servicio de pagos no tiene los mismos atributos que el usuario para el servicio de CRM: son el mismo concepto de negocio pero modelos distintos. Esa duplicación es intencional y necesaria.

Fronteras de dominio (Bounded Contexts)

El concepto de Bounded Context del Domain-Driven Design es la guía más útil para decidir dónde cortar. Un bounded context es una zona del sistema donde un modelo de datos tiene un significado preciso y consistente. El concepto «Producto» en el catálogo incluye descripción, imágenes y variantes. El concepto «Producto» en el servicio de inventario es solo un SKU con una cantidad. Son el mismo objeto del mundo real pero modelos diferentes adaptados a su contexto.

Cuando defines mal las fronteras, los servicios empiezan a acoplarse: el servicio A necesita que el servicio B tenga una estructura específica para funcionar; un cambio en B rompe A sin que nadie lo anticipe. Esta dependencia oculta en el modelo de datos es la fuente más común de fragilidad en arquitecturas de microservicios.

Svc Pedidos GET /users/{id} → necesita: name, email, address Contrato (OpenAPI) GET /users/{user_id} → 200 UserPublicDTO Svc Usuarios expone: name, email, address (DTO público) oculta: password, etc.
El contrato de API define exactamente qué expone cada servicio. El Svc Pedidos solo accede a los datos que el Svc Usuarios decide publicar en su DTO — nunca a la base de datos directamente.

Comunicación entre servicios

Hay dos patrones principales:

Un ejemplo concreto con FastAPI para el servicio de usuarios:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# DTO público — solo lo que otros servicios pueden ver
class UserPublicDTO(BaseModel):
    user_id: str
    name: str
    email: str
    shipping_address: str | None = None

# Datos internos (nunca expuestos directamente)
_users_db: dict[str, dict] = {
    "u1": {
        "name": "Ana García",
        "email": "ana@example.com",
        "password_hash": "...",       # nunca sale
        "shipping_address": "Calle Mayor 1",
        "internal_score": 98,         # nunca sale
    }
}

@app.get("/users/{user_id}", response_model=UserPublicDTO)
def get_user_public(user_id: str):
    user = _users_db.get(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return UserPublicDTO(
        user_id=user_id,
        name=user["name"],
        email=user["email"],
        shipping_address=user.get("shipping_address"),
    )

El DTO actúa como frontera explícita. Los campos internos (password_hash, internal_score) nunca abandonan el servicio. Si añades campos internos nuevos, el contrato externo no cambia y ningún consumidor se rompe.

Errores comunes

¿Cuándo no usar microservicios?

Si el equipo tiene menos de 5-8 personas, si el producto aún no tiene product-market fit, o si no hay experiencia operativa con sistemas distribuidos, un monolito modular bien estructurado es casi siempre la mejor opción. Los microservicios son una solución a problemas de escala — de equipo, de carga o de dominio. Antes de tener esos problemas, son complejidad sin beneficio.