El costo oculto que nadie menciona al principio
Un monolito bien construido necesita: un proceso de deploy, una base de datos, un sistema de logging, un sistema de monitoreo. La misma funcionalidad en 10 microservicios necesita: 10 pipelines de deploy, 10 bases de datos o schemas separados, correlación de logs entre 10 servicios, 10 conjuntos de alertas, y una capa de comunicación entre todos ellos que en el monolito era simplemente una llamada a función.
Amazon, Netflix y Spotify construyeron microservicios cuando tenían cientos de ingenieros y la escala hacía que el monolito fuera el verdadero cuello de botella. La trampa es intentar replicar su arquitectura con un equipo de 5-10 personas donde el overhead operacional elimina la mayor parte del tiempo de desarrollo.
Cuándo los microservicios SÍ tienen sentido
- Múltiples equipos trabajando en el mismo producto, con dominios bien definidos y diferente cadencia de deploy. Los microservicios permiten que el equipo de pagos deploya independientemente del equipo de catálogo.
- Componentes con requerimientos técnicos fundamentalmente distintos: el servicio de procesamiento de video necesita GPU y escala a miles de instancias efímeramente; el servicio de gestión de usuarios es liviano y constante.
- Escala diferencial: el motor de búsqueda necesita escalar a 100x en eventos de alto tráfico; el módulo de configuración de cuentas maneja 10 requests por hora.
- El monolito tiene problemas reales de escala o deploy que generan incidentes, no proyectados que justifican la complejidad desde el inicio.
Domain-Driven Design: el cimiento del diseño correcto
El error más frecuente al descomponer un monolito en microservicios: definir los servicios por capas técnicas (user-service, data-service, email-service) en lugar de por dominios de negocio. Los servicios definidos por capa técnica generan alto acoplamiento: para agregar una funcionalidad de checkout, necesitas modificar el user-service, el product-service, el order-service y el payment-service simultáneamente.
Domain-Driven Design define los Bounded Contexts — los límites naturales del negocio donde un concepto tiene una definición consistente. Un 'usuario' en el contexto de Autenticación es diferente a un 'usuario' en el contexto de Notificaciones o en el de Pagos. Cada Bounded Context se convierte en un microservicio (o en un módulo bien definido dentro del monolito).
# Bounded Contexts identificados por dominio de negocio
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Identity Context │ │ Catalog Context │ │ Orders Context │
│ ───────────────── │ │ ───────────────── │ │ ───────────────── │
│ Usuario │ │ Producto │ │ Orden │
│ Sesión │ │ Categoría │ │ LineItem │
│ Permiso │ │ Inventario │ │ Precio aplicado │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
│ │ │
└──────── Event Bus (Kafka/RabbitMQ) ─────────────┘
# Los servicios NO comparten base de datos
# Se comunican via eventos para operaciones asíncronas
# Se comunican via API para queries síncronasComunicación síncrona vs. asíncrona: la decisión más importante
Comunicación síncrona (REST/gRPC): un servicio espera la respuesta del otro antes de continuar. Más fácil de depurar, consistencia inmediata, pero crea acoplamiento temporal — si el servicio destino está caído, el servicio origen también falla.
Comunicación asíncrona (eventos via Kafka/RabbitMQ): el servicio emisor publica un evento y continúa sin esperar. El servicio consumidor procesa el evento en su propio tiempo. Más resiliente, permite escalar consumidores independientemente, pero la depuración es más compleja y la consistencia es eventual.
// Patrón Outbox para garantía de entrega de eventos
// El evento se persiste en la misma transacción que los datos
async function crearOrden(data: CreateOrderDto, trx: Transaction) {
// 1. Crear la orden en la base de datos
const orden = await Orden.create(data, { transaction: trx });
// 2. Persistir el evento en la tabla outbox (misma transacción)
await OutboxEvent.create({
aggregate_type: 'Order',
aggregate_id: orden.id,
event_type: 'OrderCreated',
payload: JSON.stringify(orden.toJSON()),
}, { transaction: trx });
// Si la transacción hace commit, el evento está garantizado
// Un worker separado lee la tabla outbox y publica a Kafka
// Si Kafka está caído, el worker reintenta al recuperarse
}Service Mesh: cuándo añadir Istio o Linkerd
Un service mesh añade una capa de proxy entre todos los microservicios que gestiona: mTLS (cifrado y autenticación entre servicios), traffic management (retries, circuit breaking, load balancing), y observabilidad (métricas de latencia y errores por par de servicios).
La decisión de cuándo añadir service mesh: es necesario cuando tienes más de 5-6 microservicios y los requerimientos de seguridad (cifrado en tránsito interno, autenticación servicio-a-servicio) o la complejidad del traffic management justifican el overhead operacional. Para 2-3 microservicios, gestionar TLS y retries a nivel de aplicación es más pragmático.
El anti-patrón del microservicio nano
El anti-patrón más destructivo: descomponer hasta el nivel de función. Un servicio que solo expone 2-3 endpoints con lógica trivial no aporta los beneficios de los microservicios — solo añade latencia de red, complejidad de deploy, y un servicio más que monitorear. La regla práctica: si un servicio no puede justificar su propio pipeline de CI/CD y su propia base de datos o schema, probablemente es demasiado pequeño.
Preguntas frecuentes
¿Cómo migro de un monolito a microservicios sin parar el negocio?
¿Microservicios o modular monolith para una startup?
¿Cómo gestiono las transacciones que cruzan múltiples microservicios?
¿Cómo hago testing de integración entre microservicios?
¿Qué base de datos usa cada microservicio?
¿Tu empresa está evaluando migrar a microservicios? Podemos hacer el assessment arquitectónico y diseñar la estrategia de migración correcta para tu contexto.
Habla con el equipo