Domain-Driven Design: Microservicios, cómo diseñar servicios alineados con los límites del negocio.
Compara un monolito modular con una separación de Sales, Payments e Inventory. Diseña contratos en TypeScript y Express y comprende fallos parciales, consistencia eventual, idempotencia, Outbox y Sagas sin introducir infraestructura compleja.
Tres servicios no garantizan tres capacidades autónomas
Sales, Payments e Inventory están en procesos distintos. Sin embargo, cada compra llama a los tres en cadena, todos leen las mismas tablas y cualquier cambio exige desplegar todo el sistema.
Tenemos una aplicación distribuida, pero poca autonomía.
La alternativa tampoco es decidir que los microservicios siempre son un error. Quizá Payments necesita un ciclo de entrega independiente, Inventory tiene una carga distinta y los equipos pueden operar esas capacidades por separado.
La pregunta es qué beneficio concreto justifica introducir límites de despliegue y comunicación por red, y qué responsabilidades nuevas tendremos que asumir.
En este artículo partiremos de un monolito modular y exploraremos una separación posible. Los diagramas y ejemplos son propuestas didácticas, no una recomendación de migrar nuestra tienda real.
Qué caracteriza a una arquitectura de microservicios
Una arquitectura de microservicios organiza la aplicación en servicios desplegables independientemente y orientados a capacidades. Esa independencia necesita contratos y límites operativos; no aparece solo al dividir un repositorio.
Lewis y Fowler describen características habituales como organización por capacidades del negocio, despliegue independiente y gestión descentralizada de datos. No proponen medir un microservicio por una cantidad fija de líneas. Microservices.
Las ventajas potenciales incluyen evolucionar o escalar capacidades de manera diferenciada. Los costes incluyen comunicación remota, más elementos operativos y resultados parciales.
«Potencial» importa: extraer código puede aumentar el coste sin obtener independencia si conserva dependencias que obligan a coordinar cada cambio.
Cuatro límites que no deben confundirse
Bounded Context: delimita un modelo y su significado.
Módulo: organiza responsabilidades dentro del código.
Aggregate: protege decisiones y consistencia dentro de un modelo.
Microservicio: introduce una unidad de ejecución, despliegue y operación.
Un contexto puede implementarse con varios módulos y agregados. Puede vivir dentro de un monolito o distribuir ciertas capacidades en más de un servicio cuando exista una razón.
No hay una regla de «un agregado, un microservicio» ni «un contexto, un servidor». Mantener coherencia del modelo y evitar fragmentación innecesaria siguen siendo decisiones de diseño.
La guía de Microsoft recomienda comenzar con el dominio y contrastar candidatos con necesidades de equipo, datos, disponibilidad y escala, revisando separaciones y agrupaciones. Eso orienta el análisis; no convierte cada candidato del modelo en un servicio obligatorio. Identificar límites de microservicios.
El punto de partida: un monolito modular
Nuestra tienda mantiene un despliegue con responsabilidades separadas:
Aplicación commerce
|
+-- Sales
| +-- modelo de pedidos
| +-- casos de uso
| +-- adaptadores
|
+-- Payments
| +-- modelo de pagos
| +-- casos de uso
| +-- adaptadores
|
+-- Inventory
+-- modelo de existencias y reservas
+-- casos de uso
+-- adaptadores
La separación debe reflejarse en dependencias y propiedad de datos. Una carpeta por contexto no basta si Sales escribe directamente las tablas de Payments.
Un monolito modular puede tener contratos explícitos entre contextos. En los ejemplos de la colección, las integraciones se expresan mediante ACL HTTP y contratos del consumidor, sin imports de entidades ajenas.
Si la llamada HTTP apunta al mismo despliegue, todavía necesita manejar errores. La ubicación física no crea una transacción compartida.
Mantener un despliegue puede simplificar operación y entrega para un equipo pequeño. No impide modelar límites que después podamos revisar.
Proponer candidatos con una razón verificable
Supongamos estas necesidades, todavía por confirmar:
Payments necesita evolucionar su integración financiera sin desplegar toda la tienda.
Inventory recibe consultas intensivas y tiene un patrón de carga diferente.
Sales puede operar y presentar estados pendientes sin obtener siempre una respuesta inmediata de todos.
Hay responsables capaces de mantener contratos y atender fallos de cada servicio.
La separación sería candidata si esas condiciones se sostienen con evidencia.
Antes de aprobarla, pregunta:
¿Qué cambios pueden entregarse realmente por separado?
¿La capacidad mantiene reglas cohesionadas o requiere llamadas constantes a sus vecinas?
¿Quién posee sus datos y sus migraciones?
¿Qué resultado ve el usuario si una dependencia falla?
¿Podemos observar y operar el recorrido completo?
¿Qué coste evita la separación y qué coste introduce?
Si la única respuesta es «queremos algo moderno», falta una razón de negocio u operación.
Una separación posible
Cliente
|
v
Sales Service -------------------> Inventory Service
| |
| datos Inventory
|
+------------------------------> Payments Service
| |
| datos Payments
|
datos Sales
Payments -- contrato de PaymentConfirmed --> Sales
Las flechas de consulta y los hechos publicados representan contratos distintos. El dibujo no requiere un broker concreto ni describe toda la coordinación de compra.
Cada servicio puede elegir una organización interna adecuada. No repetiremos aquí las capas del artículo anterior; distribuir procesos no sustituye la protección de sus reglas.
Tampoco son obligatorios repositorios separados o lenguajes diferentes. Introducir herramientas distintas sin necesidad añade mantenimiento, no autonomía por definición.
Datos propios y contratos públicos
Sales mantiene pedidos. Payments mantiene pagos e intentos. Inventory mantiene reservas y existencias.
Un servicio no debería escribir las tablas de otro para evitar una llamada. Tampoco debería depender de su esquema mediante joins operativos.
La propiedad de datos puede implementarse con bases o esquemas separados y permisos adecuados. Compartir una instancia física no equivale necesariamente a compartir propiedad, pero deben conservarse los controles y la independencia de cambios.
Si todos pueden consultar y modificar cualquier tabla, la separación de procesos pierde buena parte de su propósito.
Los informes que necesitan combinar información requieren un diseño explícito. Una consulta administrativa no justifica abrir indiscriminadamente el almacenamiento operativo de cada servicio.
Comunicación síncrona: una pregunta con un límite de espera
Sales consulta disponibilidad:
GET /integration/v1/availability/BAG-URBAN
Accept: application/json
Inventory responde:
{"sku":"BAG-URBAN","reservableQuantity":8}
La semántica es importante: observar ocho unidades no reserva ninguna. La cantidad puede cambiar antes de la siguiente operación.
El consumidor define un puerto y mantiene la interpretación del contrato en un adaptador. El siguiente ejemplo es de lectura y presupone Node.js con fetch y TypeScript configurado para sus APIs.
La configuración del origen y las credenciales procede de fuentes administrativas controladas, no de una URL enviada por el usuario. El proveedor debe autenticar y autorizar esas llamadas.
El ejemplo fija un tiempo máximo didáctico y no sigue redirecciones. No implementa límites de tamaño de respuesta, observabilidad ni gestión completa de errores de JSON; deben tratarse antes de producción.
La parte importante es la distinción: servicio inaccesible no significa producto desconocido ni disponibilidad cero.
No hay reintentos automáticos. Incluso una lectura debe tener un presupuesto total de espera y una política que no multiplique carga cuando el proveedor falla.
Solicitar cambios exige garantías adicionales
Una reserva podría usar este contrato ilustrativo:
Inventory debe definir qué significa repetir la solicitud, cómo la vincula al consumidor y qué ocurre si se reutiliza con otros datos.
Si se promete idempotencia, el registro de solicitud y el efecto deben coordinarse de forma durable y atómica. Un requestId en el JSON no implementa esa garantía.
Si Sales pierde la respuesta, no sabe automáticamente que la reserva falló. Puede necesitar consultar o repetir conforme al contrato, conservando la misma identidad lógica.
La disponibilidad de red cambia el diseño del caso de uso. Una función remota no es una función local más lenta.
Comunicación asíncrona: compartir un hecho, no una entidad
Payments puede publicar una confirmación mediante el lenguaje de integración ya estudiado:
El fragmento representa un contrato, no valida un mensaje de red. Mantiene el significado de la colección: captura completa registrada, importe bruto en céntimos de EUR e instante UTC de confirmación.
Un Domain Event interno puede originar esa información. El Integration Event selecciona una representación pública que los consumidores pueden interpretar sin importar el agregado de Payments.
El consumidor necesita verificar emisor, formato, identidad y correspondencia con su operación. No debe confirmar cualquier pedido solo porque reciba un documento con ese nombre.
La integración asíncrona permite que los servicios no tengan que estar disponibles en el mismo instante para procesar el trabajo, si el transporte conserva los mensajes adecuadamente. A cambio introduce retrasos, duplicados y posibles diferencias de orden que deben gestionarse.
Consistencia eventual con estados comprensibles
Payments puede registrar el cobro mientras Sales todavía muestra «procesando confirmación». Durante ese intervalo, sus modelos no reflejan la misma información.
La consistencia eventual no significa que todo se resolverá mágicamente. Necesita mecanismos de entrega, procesamiento, reintento y resolución de operaciones que quedan atascadas.
La experiencia debe diferenciar pendiente, rechazado y desconocido. Ocultar un estado intermedio mostrando éxito antes de tiempo no mejora la consistencia.
Una API Express de Sales podría consultar una vista local del progreso, evitando llamar a todos los servicios cada vez que el usuario actualiza la pantalla.
Este fragmento usa Express 5 y dependencias obligatorias para lectura y autorización:
El lector debe obtener datos de Sales, no de las tablas ajenas. La autorización debe verificar el permiso sobre el pedido. Se omite el middleware final de errores y no se arranca un servidor: no es una aplicación completa para producción.
La vista local tiene que actualizarse y permitir detectar estados demasiado antiguos. Leer localmente reduce una cadena de dependencias, pero no elimina el trabajo de integración.
Transactional Outbox: cerrar una brecha local
Un problema clásico aparece al guardar un pago y después intentar publicar su confirmación. Puede funcionar la escritura y fallar la publicación.
Transactional Outbox registra el cambio y una notificación pendiente en la misma transacción local. Otro proceso entrega posteriormente el mensaje. Así se evita que una de esas dos escrituras locales quede comprometida sin la otra. AWS: Transactional Outbox.
Transacción local en Payments:
guardar confirmación
registrar mensaje pendiente en outbox
confirmar transacción
Después:
entregar mensaje pendiente
registrar progreso de entrega
Puede haber duplicados si la entrega se repite. El consumidor necesita una estrategia de idempotencia apropiada y durable.
Outbox no crea atomicidad entre las bases de Payments y Sales. Tampoco sustituye las reglas para ordenar o interpretar hechos distintos. Aquí solo introducimos su propósito; no implementamos el mecanismo.
Sagas: coordinar pasos sin prometer rollback global
Una compra puede requerir reservar, cobrar y confirmar. Si una parte falla, el negocio necesita decidir cómo continuar o compensar.
Una Saga organiza una secuencia de transacciones locales y acciones de recuperación o compensación. Puede coordinarse mediante un orquestador o mediante colaboración entre participantes. La documentación de AWS describe la alternativa orquestada y sus compromisos. Saga orchestration.
Liberar una reserva o solicitar un reembolso son acciones de negocio, no un rollback instantáneo de una transacción común. Pueden fallar, tardar o necesitar intervención.
No todas las operaciones son reversibles del mismo modo. Despachar un paquete no se deshace borrando una fila.
Antes de introducir una Saga, describe el resultado aceptable y quién resuelve cada situación. No es una palabra que pueda añadirse al diagrama para declarar consistencia.
Observabilidad y pruebas del recorrido
Cuando una compra atraviesa servicios, necesitas relacionar lo ocurrido sin registrar secretos.
Mantén referencias de negocio y correlación técnica, estados de cada intento y señales de operaciones pendientes. Las trazas ayudan a seguir llamadas; las métricas muestran tendencias y los registros aportan contexto.
Distingue rechazos esperados de fallos técnicos. Un aumento de reservas rechazadas no necesariamente es una caída de Inventory.
Las pruebas también tienen niveles:
Reglas locales con dependencias controladas.
Contratos que comprueban expectativas del consumidor y proveedor.
Integraciones en entornos aislados.
Escenarios de fallo y recuperación.
Por ejemplo, estas aserciones propuestas verifican únicamente el parser del cliente anterior. No se han ejecutado como parte del artículo:
También deben probarse pérdida de respuesta, mensajes repetidos y dependencias no disponibles. Un test verde con mocks no demuestra que todo el sistema pueda recuperarse.
Cuándo conservar el monolito modular
Mantener un despliegue puede ser adecuado cuando los límites siguen cambiando, el equipo es pequeño o no existe una necesidad clara de entregas y operación independientes.
También puede ser la mejor opción si una separación produciría demasiadas llamadas para tomar cada decisión o si todavía no hay capacidad para mantener los servicios resultantes.
Un monolito con límites claros puede aportar más autonomía de código que varios servicios que comparten tablas y se despliegan siempre juntos.
Si existe un candidato justificado, extraer una capacidad de manera gradual permite comprobar el beneficio antes de fragmentar toda la aplicación. Hay que planificar propiedad de datos, transición y operación; evitar dos escritores sin un acuerdo explícito.
No conviene distribuir un acoplamiento que todavía no se entiende.
Señales de un sistema distribuido fuertemente acoplado
Revisa estas situaciones:
Se crea un servicio por entidad sin una capacidad operativa clara.
Todos los cambios requieren modificar y desplegar varios servicios.
Una petición encadena Sales, Payments e Inventory para cada lectura.
Los servicios escriben en tablas ajenas.
Las bibliotecas compartidas obligan a coordinar todos los modelos.
No existe responsable para una compra que queda pendiente.
Se reintentan mutaciones sin una garantía de repetición.
Una llamada remota puede cruzar una frontera coherente. Muchas llamadas para reconstruir una decisión pequeña pueden indicar un límite que necesita revisarse.
Dos ejemplos para contrastar
Educación: aislar una carga específica
Una plataforma puede tener una capacidad de evaluación que necesita recursos y operación muy distintos de la gestión de cursos. Esa diferencia puede justificar estudiar un servicio separado si su contrato y sus fallos están claros.
No implica separar cada pregunta, intento y calificación en procesos distintos. La cohesión de la capacidad sigue importando.
Reservas: fragmentar una decisión demasiado pronto
Una aplicación pequeña separa disponibilidad y reserva en servicios que necesitan consultarse constantemente para evitar solapamientos sobre el mismo recurso.
Antes de añadir más coordinación distribuida, conviene revisar si esa decisión debería permanecer junta dentro de un modelo o servicio.
Ambos casos enseñan a buscar una razón concreta de autonomía, no un tamaño mínimo de código.
Evitar que la IA fragmente por defecto
Para Cursor, Codex o Claude, proporciona requisitos y alternativas permitidas.
Para analizar límites:
Compara mantener este monolito modular con extraer una capacidad. Justifica cada candidato por cohesión, datos, equipo y despliegue. No asignes un microservicio a cada agregado. Distingue evidencia de hipótesis y señala costes nuevos.
Para generar una integración:
Implementa solo el adaptador HTTP de Sales según este contrato de Inventory. Valida datos desconocidos y distingue producto desconocido, disponibilidad cero y fallo técnico. No importes entidades externas ni supongas una transacción compartida. No agregues reintentos de mutaciones sin garantías documentadas.
Para revisar el proceso:
Recorre esta compra con pérdida de respuesta y mensaje duplicado. Identifica estados intermedios, responsables y garantías pendientes. No afirmes que Outbox entrega exactamente una vez ni que una Saga revierte automáticamente efectos remotos.
Una persona debe revisar viabilidad operativa y límites del negocio. Un conjunto de carpetas y Dockerfiles generado por IA no demuestra una arquitectura autónoma.
Ejercicios
1. La base compartida
Payments escribe su tabla, pero Sales la actualiza directamente para resolver incidencias. ¿Qué autonomía se ha comprometido?
2. El timeout del cobro
Sales no recibe respuesta a una solicitud de pago. ¿Puede iniciar otro intento con una referencia nueva suponiendo que el anterior falló?
3. El servicio por agregado
Un contexto tiene cinco agregados. El equipo propone cinco microservicios como regla automática. ¿Qué análisis falta?
Soluciones razonadas
1. La propiedad de datos y la capacidad de evolucionar reglas y esquema sin consumidores que las eludan. La resolución de incidencias necesita una capacidad autorizada o un procedimiento explícito, no escritura arbitraria entre servicios.
2. No. El cobro puede haberse realizado. Hay que consultar o repetir según el contrato de la misma operación lógica, evitando introducir otro efecto por desconocer el resultado.
3. Cohesión funcional, dependencia de decisiones, propiedad de datos, necesidades de despliegue, equipo y operación. Los agregados pueden orientar candidatos, pero no fijan por sí solos unidades de servicio.
Distribuir cuando la autonomía compensa el coste
DDD aporta límites y lenguaje para discutir servicios alineados con el negocio. La red, el despliegue y la operación añaden decisiones que esos límites conceptuales no resuelven automáticamente.
Antes de separar, comprueba qué independencia obtendrás, qué datos poseerá cada servicio y cómo reconocerás fallos parciales.
Un monolito modular y una arquitectura de microservicios pueden respetar modelos bien diseñados. La elección debe justificarse por las necesidades del sistema y la capacidad de sostener sus garantías.
Integra la colección construyendo una API de pedidos con TypeScript, Express, Zod y SQLite. Incluye los archivos del proyecto, reglas del dominio, eventos, control de versiones, pruebas con Vitest y comandos para ejecutarla y verificarla localmente.
Aprende a orientar agentes de IA con reglas del negocio, límites de contexto y criterios verificables. Un caso en TypeScript muestra cómo preparar instrucciones, proteger Order, diseñar pruebas y revisar cambios sin delegar el criterio arquitectónico.
Refactoriza la confirmación de pedidos desde un controlador acoplado hacia Order, caso de uso, puerto y adaptadores. Aprende la regla de dependencias, composición manual y pruebas sin infraestructura, con TypeScript y Express.