01 · Hexagonal · DDD · CQRS · EDA

1. El desafío de la complejidad en sistemas modernos

La experiencia muestra que, en aplicaciones monolíticas de gran tamaño, hasta un 80% del tiempo del equipo se invierte en leer código y rastrear dependencias antes de poder realizar un cambio seguro. Cada nueva funcionalidad incrementa la probabilidad de efectos colaterales, de modo que la evolución del producto se ralentiza y el refactor pasa de ser una decisión técnica a convertirse en una cuestión política:

«¿Quién se atreve a tocar ese módulo crítico sin romper el build justo antes del despliegue?»

Para visualizar el impacto, comparemos un monolito acoplado con un conjunto de microservicios estructurados en capas de dominio:

spinner

Mientras el monolito concentra responsabilidades y dependencias en un mismo espacio de memoria, el enfoque hexagonal distribuye la lógica en componentes aislados y explícitamente conectados.


2. Un enfoque arquitectónico estratégico

A lo largo del curso profundizaremos en cuatro pilares complementarios. A continuación se presenta una síntesis conceptual y sus compromisos.

2.1 Arquitectura Hexagonal (Ports & Adapters)

El propósito fundamental es proteger el núcleo de dominio de detalles tecnológicos que inevitablemente cambian.

  • Dónde encaja: capa de dominio y casos de uso internos.

  • Resultado esperado: adaptar o sustituir tecnología (BD, mensajería) sin reescribir reglas de negocio.

Ventaja
Coste / Disciplina requerida

Dominio libre de frameworks

Mayor código de infraestructura inicial

Tests unitarios sin I/O

Definir contratos de puerto claros

Cambio de proveedor de base de datos en un sprint

Contener dependencias externas mediante inversión de control

Ejemplo mínimo (TypeScript)

Observación: el core no sabe que existe Prisma ni PostgreSQL.


2.2 Domain-Driven Design (DDD)

DDD parte de una premisa sencilla: el software se mantiene alineado con la realidad del negocio cuando modela el lenguaje y las reglas que los expertos usan a diario. Aplicarlo exige dos niveles:

Nivel
Objetivo
Artefactos

Estratégico

Domar la complejidad global dividiendo el dominio en zonas coherentes.

Subdominios, Bounded Contexts, Context Map

Táctico

Expresar las reglas dentro de cada contexto.

Entities, Value Objects, Aggregates, Domain Events, Domain Services


2.2.1 Ejemplo estratégico — Context Map

Supongamos un comercio electrónico:

spinner
  • Ordering y Payments son dominio Core: diferencian al negocio.

  • Inventory y Shipping proveen soporte, pero no ventaja competitiva.

Decisión: invertimos más esfuerzo DDD en los contextos core y aplicamos soluciones genéricas (o SaaS) en los de soporte.


2.2.2 Ejemplo táctico — Entidad, Value Object y Aggregate

Puntos clave

  1. Invariantes encapsuladas: no existe forma de dejar el agregado en estado ilegítimo sin lanzar excepción.

  2. Eventos de dominio: el agregado comunica un cambio significativo (OrderPaid) sin acoplarse a infraestructura.

  3. Value Object Money preserva exactitud y reglas monetarias (inmutabilidad, no valores negativos).


2.2.3 Domain Service — regla que cruza aggregates

  • La operación involucra el agregado Order y un sistema externo (Credit Service).

  • Encapsularla en un Domain Service mantiene a la entidad Order pura e independiente de APIs externas.


2.2.4 Beneficios tangibles de aplicar DDD correctamente

Métrica
“Modelo anémico”
DDD táctico

Cobertura tests (target)

20 - 30 %

80 % (foco en dominio)

Tiempo medio de cambio (MTTR)

Alto – revisar capas front/backend/DB

Bajo – reglas localizadas en Aggregates

Comunicación negocio-equipo

Glosario disperso

Lenguaje Ubicuo

Resultado final: el costo de agregar reglas nuevas se aproxima al costo de escribir las reglas, no al costo de redescubrir el sistema.

2.2.5 Ejercicio — Domain Model

Modelar el/los dominio(s) de https://www.eachlabs.ai/ - basado en su descripción.


2.3 CQRS (Command Query Responsibility Segregation)

Idea principal: separar la modificación de estado (Commands) de la consultación de datos (Queries), permitiendo que cada lado evolucione, escale y se optimice de manera independiente.

2.3.1 Modelos independientes

  • Write Model: contiene las reglas de negocio, las invariantes y las transacciones. Suele usar el modelo de dominio (Aggregates, Entities, Value Objects) junto a un ORM o repositorios transaccionales.

  • Read Model: diseñado para consultas rápidas y específicas, a menudo desnormalizado en tablas o índices optimizados (por ejemplo, vistas materializadas, bases de datos NoSQL).

2.3.2 Flujo típico

spinner

2.3.3 Ejemplo de implementación mínima (TypeScript)

2.3.4 Consideraciones

  • Consistencia eventual: tras un comando, puede haber un pequeño retardo hasta que el Read Model refleje el cambio. Hay que diseñar UX y Sagas acordes.

  • Idempotencia y resiliencia: al procesar eventos para actualizar el Read Model, los handlers deben ser idempotentes y capaces de reintentos seguros.

  • Event Sourcing (opcional): si los eventos de dominio se convierten en la única fuente de verdad, el Write Model se reconstruye reaplicando la secuencia de eventos. Esto facilita auditoría y versionado, pero añade complejidad operacional.

2.3.5 ¿Cuándo usarlo?

  • Alta carga de lectura y escritura que requieren escalado independiente.

  • Modelos de datos muy divergentes entre operaciones de lectura (dashboards, reportes) y escritura (flujos de negocio complejos).

  • Equipo multidisciplinar, donde un grupo optimiza el Write Model y otro el Read Model sin interferirse.


2.4 Event-Driven Architecture (EDA)

Idea principal: articular el sistema en torno a la emisión y consumo de eventos de dominio a través de un broker asíncrono (RabbitMQ, Kafka, AWS SNS/SQS…), consiguiendo desacoplamiento, flexibilidad y resiliencia.

2.4.1 Patrón de Publicación–Suscripción

  • Productor: el Aggregate o el Application Service publica un evento (por ejemplo, OrderPaid, InventoryReserved).

  • Consumidor: uno o varios microservicios o Read Updaters se suscriben al tópico correspondiente y reaccionan (actualizan proyecciones, envían notificaciones, disparan nuevas acciones).

2.4.2 Esquema de evento

2.4.3 Ejemplo mínimo de publicación y suscripción (Node.js + RabbitMQ)

2.4.4 Patrones de fiabilidad

Patrón
Objetivo

Outbox

Garantizar que el evento se guarda y publica en la misma transacción.

Idempotencia

Permitir reintentos sin efectos colaterales al procesar eventos duplicados.

Dead-Letter Queue

Aislar los mensajes que fallan repetidamente para revisión manual.

Circuit Breaker

Evitar que un consumidor defectuoso bloquee la cadena de procesamiento.

2.4.5 Orquestación vs. Coreografía

  • Coreografía: cada servicio reacciona a eventos de otros sin coordinador central. Máximo desacoplo, pero puede ser difícil razonar el flujo.

  • Orquestación: un orquestador (por ejemplo, un Saga Manager) envía comandos o eventos en un flujo controlado. Más control, pero introduce un punto de coordinación que debe gestionarse.

2.4.6 Anti-patrón

Eventos como RPC encubierto: esperar respuesta sincrónica (pub/sub sincrónico) rompe el desacoplamiento y reintroduce latencias, time-outs y complejidad.


3. ¿Por qué funciona bien en Node.js?

Característica de Node
Beneficio para el enfoque del curso

Modelo de ejecución asíncrono y no bloqueante

Manejo eficiente de brokers y bases de datos concurrentes

Ecosistema npm extenso

Adapters disponibles para RabbitMQ, gRPC, OpenTelemetry, etc.

TypeScript maduro

Expresión clara de Entities, Value Objects y puertos con tipos estáticos

Imágenes Docker ligeras

Facilita microservicios con arranque rápido y bajo consumo

Herramientas de testing (Vitest, ts-mockito)

Permite pruebas rápidas en memoria del núcleo de dominio

Ejemplo de instrumentación OTel mínima:


4. Beneficios clave

  • Mantenibilidad: cambio localizado sin efectos cascada.

  • Escalabilidad: lectura y escritura crecen de forma independiente.

  • Time-to-Market: incorporación de nuevos features con menor riesgo.

  • Observabilidad: capas y eventos claramente delineados facilitan el trazado.

Última actualización