Arquitectura de Software

Domain-Driven Design: Context Mapping, cómo diseñar las relaciones entre Bounded Contexts.

Aprende a construir un Context Map y elegir cómo colaboran los modelos y equipos. Compara ocho patrones de integración, distingue Upstream y Downstream y aplica una ACL breve en TypeScript sin introducir microservicios.

Cargando tus opciones de aprendizaje…

La API funciona, pero los equipos siguen bloqueados

El equipo de Sales necesita saber si puede reservar dos mochilas. Inventory ofrece una API que responde correctamente, pero su contrato cambia sin aviso. Billing, por su parte, interpreta «pedido confirmado» como «pedido listo para facturar», mientras Sales también confirma operaciones cuya facturación requiere una revisión posterior.

Las llamadas devuelven respuestas válidas. El problema está en los acuerdos: quién decide el significado de los datos, quién puede pedir cambios y quién debe adaptarse cuando el otro modelo evoluciona.

Ya sabemos que un Bounded Context delimita un modelo coherente. Ahora vamos a diseñar las relaciones entre esos modelos. Usaremos Catalog, Sales, Inventory y Billing, primero como contextos de un monolito modular. No necesitamos distribuir la aplicación para descubrir que sus equipos y contratos dependen unos de otros.

Qué representa un Context Map

Un Context Map documenta los contextos que participan en un sistema y las relaciones relevantes entre ellos. Context Mapping es el trabajo de descubrir, discutir y mantener esas relaciones.

El mapa debe ayudarte a responder preguntas concretas: ¿de quién depende Sales para interpretar una oferta?, ¿quién negocia el contrato de reservas?, ¿dónde se traduce el lenguaje de Sales al de Billing?, ¿qué parte del modelo se comparte deliberadamente?

Eric Evans describe el Context Map como una representación de los contextos y sus puntos de contacto, incluyendo traducciones y acuerdos de intercambio. Su referencia también incorpora patrones que expresan la relación entre los equipos. DDD Reference, Context Map y patrones de relación.

Conviene distinguir dos versiones del mapa:

  • Actual: refleja las dependencias y acuerdos que existen, incluso cuando son incómodos.
  • Objetivo: describe cambios propuestos, con responsables y pasos para llegar a ellos.

Si dibujas una relación ideal de colaboración donde hoy nadie negocia cambios, ocultas el riesgo que necesitas gestionar.

No equivale a un diagrama de infraestructura

«Sales llama a Inventory por HTTP» describe un mecanismo. Todavía no dice quién controla el contrato, qué significa reservar ni cómo se acuerda una modificación incompatible.

Un diagrama de infraestructura puede mostrar servidores y redes. Uno de secuencia puede ordenar peticiones. El Context Map incorpora otra información: dependencia entre modelos, traducción, responsabilidades y capacidad de coordinación.

Puedes referenciar esos otros diagramas, pero no necesitas dibujar todos sus detalles dentro del mapa.

Upstream y Downstream: dependencia, no recorrido de una petición

En una relación asimétrica, llamamos Upstream al lado cuyo modelo o decisiones condicionan al otro. Downstream es el lado que depende de ellos y debe considerar su evolución.

Supongamos que Inventory define los estados y las reglas públicas de una reserva. Sales consume ese contrato. Para esa relación:

Inventory [U] ---- contrato de reservas ----> Sales [D]

Leyenda de este artículo: U significa Upstream, D significa Downstream y la flecha apunta hacia el contexto dependiente. No representa el sentido de una petición HTTP ni el orden temporal del negocio.

Aunque Sales envíe la petición y reciba la respuesta, sigue siendo Downstream respecto del contrato ofrecido por Inventory. Un servidor HTTP no es Upstream por definición: observa la dependencia semántica y quién condiciona los cambios.

Además:

  • Un contexto puede ser Upstream en una relación y Downstream en otra.
  • Downstream no significa menor importancia comercial.
  • El proveedor puede tener que atender necesidades del consumidor mediante acuerdos explícitos.
  • Dos modelos con necesidades recíprocas no deben recibir etiquetas asimétricas por comodidad.

Si cada cambio requiere negociar en ambos sentidos, investiga si existe una colaboración simétrica, dos relaciones diferentes o una separación que todavía no está bien definida.

Ocho patrones para elegir una relación

Los patrones siguientes pertenecen al vocabulario de Context Mapping de Evans. Sus descripciones aquí están adaptadas y los ejemplos son decisiones didácticas, no un mapa universal del comercio electrónico. La referencia original permite consultar sus formulaciones y matices. Eric Evans: DDD Reference, páginas 30–37 del documento.

1. Partnership: coordinar porque el resultado depende de ambos

Dos equipos acuerdan trabajar como socios cuando sus entregas se necesitan mutuamente. Coordinan planificación, integración y cambios para que las dos partes puedan tener éxito.

Imagina que Sales e Inventory lanzan juntos una nueva modalidad de reserva para ventas anticipadas. Ninguno puede entregar la capacidad por separado y ambos pueden comprometerse con el mismo objetivo.

Aporta: comunicación directa y decisiones conjuntas ante cambios que afectan a ambos.

Cuesta: coordinación sostenida. Si un equipo no puede asumir ese compromiso, escribir Partnership en el mapa no lo crea.

No exige compartir todas las clases ni tener un único calendario para cualquier tarea. Sí exige acuerdos reales sobre la colaboración que sostiene esa funcionalidad.

2. Shared Kernel: compartir poco y gobernarlo juntos

Los equipos acuerdan compartir una parte pequeña del modelo, junto con el código o los elementos necesarios para mantenerla consistente. Sus cambios necesitan coordinación.

En una plataforma de reservas, dos contextos podrían compartir una definición cuidadosamente acordada de intervalo temporal. Antes de hacerlo, deben comprobar que ambos interpretan igual sus extremos, zona horaria y reglas pertinentes.

Aporta: evita traducir repetidamente una parte que de verdad tiene el mismo significado.

Cuesta: introduce un área de cambio conjunto. El núcleo deja de ser pequeño si termina conteniendo entidades completas, reglas particulares y utilidades sin relación.

Un paquete de logging no es un Shared Kernel del dominio. Tampoco lo es una librería que un equipo impone unilateralmente. En nuestro ejemplo principal elegiremos contratos y traducción, sin importar modelos internos entre contextos.

3. Customer/Supplier Development: negociar necesidades Downstream

Hay una dependencia asimétrica, pero el equipo Upstream incorpora necesidades del Downstream en su planificación. «Customer» aquí describe el papel del equipo consumidor, no al comprador de la tienda.

Inventory ofrece reservas y Sales necesita distinguir una reserva aceptada de una rechazada. Ambos acuerdan estados, fechas de entrega y pruebas de aceptación.

Aporta: influencia explícita del consumidor y compromisos verificables del proveedor.

Cuesta: requiere tiempo, prioridades negociadas y capacidad de cumplir. No equivale a que Sales tenga veto sobre cualquier cambio interno de Inventory.

Una reunión ocasional o un endpoint disponible no demuestran que exista esta relación.

4. Conformist: adoptar el modelo ofrecido

El Downstream acepta el modelo del Upstream y evita mantener una traducción propia sustancial. Es una opción cuando no puede influir en el proveedor y adaptarse a su lenguaje resulta asumible.

Un pequeño portal que reproduce el seguimiento de un transportista puede decidir usar sus estados tal como se ofrecen, si ese lenguaje sirve a su propósito.

Aporta: menor esfuerzo de traducción y un comienzo más sencillo.

Cuesta: la evolución del proveedor condiciona al consumidor. Si el negocio local empieza a necesitar otro significado, la comodidad inicial puede convertirse en acoplamiento costoso.

Conformarse con el modelo no implica confiar ciegamente en las respuestas: la validación y la seguridad siguen siendo necesarias.

5. Anti-Corruption Layer: proteger el modelo consumidor

Una Anti-Corruption Layer, o ACL, traduce la interacción con un sistema externo al lenguaje que necesita el contexto consumidor. Puede ser pequeña; no requiere otro servicio ni una plataforma de integración.

Si Billing necesita «unidades reservadas para este pedido» y Inventory habla de asignaciones con códigos propios, Billing puede mantener una traducción en su borde.

Aporta: evita que esos códigos y supuestos se propaguen por el modelo local.

Cuesta: hay que mantener la traducción y detectar cambios de significado. Una ACL no elimina la dependencia del proveedor ni inventa capacidades que este no ofrece.

Un wrapper que solo renombra una función, pero devuelve el modelo externo completo, puede dejar intacto el problema.

6. Open Host Service: ofrecer una integración estable a varios consumidores

El Upstream ofrece un protocolo público de integración que atiende necesidades de múltiples consumidores, en lugar de crear una conexión particular para cada uno.

Inventory podría exponer capacidades de consulta y reserva para Sales, operaciones de almacén y otros consumidores autorizados.

Aporta: un punto de integración mantenido y comprensible.

Cuesta: el proveedor asume compatibilidad, documentación y evolución del protocolo. Diseñar para varios consumidores exige evitar que el contrato se convierta en la suma de todas sus particularidades.

«Open» no significa acceso anónimo o gratuito. Un Open Host Service puede exigir autenticación, autorización y restricciones de uso.

7. Published Language: acordar cómo interpretar lo intercambiado

Un Published Language define un lenguaje documentado para expresar la información compartida. Incluye significado, no solamente formato.

Por ejemplo: una confirmación de venta identifica pedido, condiciones aceptadas, moneda y momento relevante, con reglas explícitas de interpretación.

Aporta: un vocabulario común de intercambio sin obligar a un modelo interno único.

Cuesta: requiere gobernar su evolución y aclarar ambigüedades. Un JSON válido no explica por sí solo si un importe incluye impuestos o si «confirmed» permite emitir una factura.

Puede acompañar a un Open Host Service. No son sinónimos: uno describe la oferta de integración; el otro, el lenguaje que se intercambia.

8. Separate Ways: decidir que integrar no compensa

Dos contextos siguen caminos separados cuando el beneficio de integrarlos no justifica su coste.

Un sitio educativo puede mantener su herramienta de reservas de talleres independiente de una biblioteca pública de artículos. Si no existe una necesidad de negocio que los conecte, no hace falta sincronizar usuarios o categorías solo porque ambos usan la palabra «tema».

Aporta: autonomía y menos dependencias.

Cuesta: puede haber duplicación o tareas manuales aceptadas conscientemente.

Si una persona exporta e importa archivos para conectar ambos procesos, documenta esa dependencia. No la conviertas en Separate Ways únicamente porque no haya llamadas HTTP.

Los patrones describen aspectos que pueden combinarse

No elijas un único patrón para toda la aplicación.

Inventory puede ofrecer un Open Host Service con un Published Language. Sales puede negociar requisitos mediante Customer/Supplier y mantener, a la vez, su ACL para proteger su vocabulario.

Cada decisión responde una pregunta distinta:

  • Coordinación: ¿los equipos se comprometen como socios o negocian una relación asimétrica?
  • Modelo: ¿comparten una parte, adoptan el modelo externo o lo traducen?
  • Oferta de integración: ¿existe un servicio público mantenido?
  • Lenguaje de intercambio: ¿hay semántica documentada?
  • Necesidad: ¿conviene conectar estos contextos?

No confundas combinar aspectos compatibles con etiquetar una relación con todo. Para el mismo alcance, adoptar el modelo por completo y proteger un modelo distinto mediante traducción son decisiones diferentes.

Construir el mapa de la tienda, paso a paso

1. Anotar las dependencias actuales

Partimos de esta situación hipotética:

  • Sales lee descripciones comerciales de Catalog.
  • Sales pide reservas a Inventory.
  • Billing usa condiciones confirmadas de Sales.
  • Catalog no necesita datos de Billing.

Todavía falta averiguar si los equipos negocian cambios y cómo interpretan esos datos.

2. Preguntar quién controla cada acuerdo

Catalog mantiene un contrato de presentación que Sales considera suficiente. Inventory puede negociar necesidades de reservas. Sales acuerda con Billing qué significa una venta facturable.

Eso nos permite proponer el siguiente mapa objetivo:

Catalog [U] ---- presentación ----------------> Sales [D]
  Proveedor: OHS + PL                 Consumidor: Conformist

Inventory [U] -- reservas --------------------> Sales [D]
  Equipos: Customer/Supplier
  Proveedor: OHS + PL                 Consumidor: ACL

Sales [U] ------ condiciones confirmadas -----> Billing [D]
  Equipos: Customer/Supplier
  Intercambio: PL                     Consumidor: ACL

OHS abrevia Open Host Service; PL, Published Language. Son abreviaturas declaradas para este artículo, no una notación obligatoria. Las flechas mantienen el sentido de dependencia definido antes.

En Catalog–Sales, Conformist se limita a los datos de presentación consumidos. Sales conserva sus reglas comerciales propias; no adopta automáticamente todo el modelo del catálogo.

3. Justificar una relación en una ficha

Para Inventory–Sales:

Necesidad: Sales debe saber si una reserva fue aceptada para el pedido solicitado.
Upstream: Inventory, responsable del contrato de reservas.
Downstream: Sales, responsable de interpretar el resultado en su proceso comercial.
Coordinación: Customer/Supplier; las necesidades acordadas entran en la planificación de Inventory.
Protección: ACL de Sales que convierte códigos externos en resultados locales.
Evolución: avisar cambios incompatibles y verificar ejemplos de aceptación del contrato.
Fuera del acuerdo: Sales no decide cómo Inventory distribuye unidades entre ubicaciones.

Ahora hay algo que los equipos pueden revisar. El diagrama aislado no muestra todos esos compromisos.

4. Recorrer un cambio concreto

Inventory quiere agregar el estado «pendiente de revisión». Si Sales solo entiende aceptado o rechazado, no puede convertir el nuevo estado en aceptado por defecto.

La relación Customer/Supplier permite negociar la semántica y el momento del cambio. La ACL concentra la adaptación. El contrato y sus pruebas deben reflejar la nueva decisión antes de utilizarla.

El resultado observable esperado es que Sales conserve una interpretación explícita. No estamos afirmando que la ACL haga compatible cualquier cambio automáticamente.

Una ACL breve en TypeScript

Este ejemplo didáctico muestra únicamente la traducción. No implementa HTTP, persistencia ni reserva real. El adaptador HTTP de Sales recibiría datos desconocidos, comprobaría el formato y luego llamaría a esta función con el tipo validado.

// Representación del proveedor, localizada en el adaptador de Sales.
// No se importa una entidad interna de Inventory.
interface InventoryReservationReply {
  readonly reservation_id: string;
  readonly order_reference: string;
  readonly result_code: "ALLOCATED" | "DENIED";
}

type SalesReservation =
  | { readonly status: "accepted"; readonly reservationId: string }
  | { readonly status: "rejected" };

function translateReservation(
  reply: InventoryReservationReply,
  expectedOrderId: string,
): SalesReservation {
  if (reply.order_reference !== expectedOrderId) {
    throw new Error("La reserva pertenece a otro pedido");
  }

  switch (reply.result_code) {
    case "ALLOCATED":
      if (reply.reservation_id.trim().length === 0) {
        throw new Error("Falta la referencia de la reserva aceptada");
      }
      return {
        status: "accepted",
        reservationId: reply.reservation_id,
      };

    case "DENIED":
      return { status: "rejected" };

    default:
      throw new Error("Estado de reserva no soportado");
  }
}

La traducción expresa el acuerdo de este ejemplo: ALLOCATED significa reserva aceptada, no mercancía enviada. Si Inventory usa ese código para una asignación provisional, el acuerdo sería incorrecto aunque el código compilase.

El tipo de TypeScript no valida un JSON recibido por red. El adaptador debe rechazar campos inválidos, estados desconocidos y respuestas que no correspondan a la operación solicitada. El caso por defecto refuerza una respuesta explícita ante valores inesperados, pero no sustituye esa validación.

Podemos describir tres comportamientos mediante aserciones. Este bloque complementa al anterior y no utiliza dependencias externas:

function assertCondition(condition: boolean, message: string): void {
  if (!condition) throw new Error(message);
}

const accepted = translateReservation({
  reservation_id: "reservation-20",
  order_reference: "order-100",
  result_code: "ALLOCATED",
}, "order-100");

assertCondition(
  accepted.status === "accepted" &&
    accepted.reservationId === "reservation-20",
  "Debe conservar la referencia de una reserva aceptada",
);

const rejected = translateReservation({
  reservation_id: "",
  order_reference: "order-100",
  result_code: "DENIED",
}, "order-100");

assertCondition(
  rejected.status === "rejected",
  "Una denegación no debe convertirse en aceptación",
);

let wrongOrderRejected = false;
try {
  translateReservation({
    reservation_id: "reservation-21",
    order_reference: "order-999",
    result_code: "ALLOCATED",
  }, "order-100");
} catch {
  wrongOrderRejected = true;
}

assertCondition(
  wrongOrderRejected,
  "Debe rechazar una respuesta perteneciente a otro pedido",
);

Estas comprobaciones protegen la interpretación local y la identidad de la operación. No prueban disponibilidad del proveedor ni compatibilidad con su API real. Para eso necesitamos pruebas del contrato y del adaptador.

La ACL pertenece al consumidor. No autoriza a Sales a escribir directamente las tablas de Inventory. Y si la integración usa HTTP, la llamada no forma automáticamente parte de una transacción local de Sales.

Un evento también necesita una relación documentada

Sales puede ofrecer a Billing un contrato de integración basado en una venta confirmada. El mecanismo podría ser una notificación o una consulta; el mapa debe explicar la dependencia y el significado independientemente del transporte.

Un Domain Event interno no se vuelve un Published Language por serializarlo. Hay que seleccionar la información compartida, documentar sus términos y acordar su evolución.

Si Billing exige condiciones adicionales para emitir un documento, su modelo debe conservar esa decisión. El nombre del evento de Sales no debería imponer silenciosamente una regla fiscal o administrativa que Sales no conoce.

Dos decisiones fuera del comercio electrónico

Educación: compartir deliberadamente una parte pequeña

Inscripciones y planificación académica usan un intervalo de fechas con el mismo significado y necesitan evolucionarlo juntos. Un Shared Kernel puede ser razonable si ambos equipos aceptan su gobierno y su alcance permanece pequeño.

La comprobación decisiva es semántica: si planificación necesita intervalos abiertos e inscripciones trabaja con fechas límite inclusivas, quizá no comparten el mismo concepto. En ese caso, una traducción puede ser más clara.

Este ejemplo enseña a no compartir una clase solo porque sus propiedades coinciden.

Logística: aceptar un modelo externo o traducirlo

Un panel informativo muestra los estados del transportista sin añadir reglas. Puede adoptar un enfoque Conformist y reducir el trabajo de integración.

Un sistema de atención al cliente, en cambio, decide si una entrega habilita una reclamación. No debería confundir «incidencia registrada» con «pérdida confirmada». Una ACL ayuda a separar el lenguaje del transportista de esas decisiones locales.

La diferencia no está en el proveedor: está en las necesidades y responsabilidades del consumidor.

Usar el mapa para orientar a una IA

Un mapa resulta útil para Cursor, Claude o Codex cuando lo acompañas de acuerdos verificables. Un dibujo sin leyenda o sin distinguir situación actual y propuesta puede provocar recomendaciones equivocadas.

Tres prompts posibles:

Para descubrir relaciones:

Con estas fichas de Catalog, Sales, Inventory y Billing y estos ejemplos de interacción, construye un Context Map actual. Identifica U/D por dependencia del modelo, no por dirección HTTP. Separa hechos de hipótesis. Devuelve una leyenda, relaciones y preguntas pendientes; no inventes acuerdos entre equipos.

Para proponer un cambio:

Sales consume reservas de Inventory mediante Customer/Supplier y una ACL HTTP propiedad de Sales. Propón cómo incorporar un nuevo estado pendiente sin tratarlo como aceptado. Mantén contratos del consumidor y no importes entidades de Inventory. Entrega cambios mínimos del contrato, traducción y casos de prueba; no agregues infraestructura.

Para revisar código:

Contrasta este diff con el mapa adjunto. Busca importaciones entre contextos, exposición de modelos internos, adopción accidental de códigos externos y cambios incompatibles. Explica el comportamiento afectado y distingue incumplimientos del acuerdo de preferencias de diseño.

Una persona debe comprobar que los acuerdos descritos existen y que los ejemplos reflejan la semántica del negocio. La IA puede detectar inconsistencias; no puede negociar compromisos reales por los equipos.

Ejercicios para elegir relaciones

1. El proveedor que no negocia

Un proveedor externo ofrece un modelo de devoluciones fijo. Tu sistema necesita distinguir devolución solicitada de reembolso aprobado. ¿Basta con marcar Customer/Supplier?

2. La librería compartida

Dos equipos comparten una clase Customer de treinta campos. Cada cambio rompe al otro. Proponen llamarla Shared Kernel y continuar igual. ¿Qué revisarías?

3. Las flechas de las reservas

Sales envía peticiones a Inventory. Un diagrama marca Sales como Upstream porque inicia la comunicación. Inventory decide el contrato y Sales adapta sus resultados. ¿Cómo corregirías el mapa?

Soluciones razonadas

1. No. Customer/Supplier requiere incorporar necesidades del consumidor en la planificación del proveedor. Si eso no ocurre, documentaría la dependencia real. Como los significados locales importan, propondría una ACL que no confunda los estados; si el proveedor no ofrece información suficiente, debe quedar una limitación explícita.

2. El nombre no crea gobierno conjunto ni coherencia semántica. Revisaría qué conceptos tienen realmente el mismo significado, qué parte mínima necesitan compartir y cómo coordinan sus cambios. Si las reglas difieren, separaría modelos y definiría un contrato en lugar de ampliar la entidad global.

3. Para esa relación, Inventory es Upstream y Sales Downstream. La flecha de dependencia puede apuntar de Inventory a Sales, con una leyenda explícita. Si hace falta mostrar petición y respuesta, usaría un diagrama de secuencia separado. Después documentaría si Sales negocia requisitos, se conforma o traduce el modelo.

Mantener el mapa útil

Revisa el mapa cuando cambie un contrato, aparezca un consumidor o se modifique quién puede decidir sobre la integración. No hace falta actualizarlo por cada cambio interno que no afecta a la relación.

Antes de aceptar una relación, comprueba tres cosas: puedes explicar la dependencia, el equipo responsable reconoce el acuerdo y existe un ejemplo que muestra cómo se interpreta el contrato.

Un mapa pequeño con relaciones honestas permite decidir qué negociar, qué traducir y qué dejar independiente. Puede servir al mismo monolito modular durante años; el número de servidores no determina su utilidad.

Cargando tus opciones de aprendizaje…

Cargando conversación…

Sigue aprendiendo