Arquitectura de Software

Domain-Driven Design: Bounded Contexts, cómo definir los límites del dominio y construir modelos independientes.

Aprende a delimitar modelos y vocabulario en catálogo, ventas, inventario y facturación. Distingue contextos, subdominios y módulos, diseña contratos explícitos y usa estos límites para orientar el código generado con IA, sin necesitar microservicios.

Cargando tus opciones de aprendizaje…

Cuando «producto» empieza a significar cuatro cosas

Imagina una tienda que vende una mochila. Para catálogo, el producto necesita una descripción, fotografías y un estado de publicación. Para ventas, importa qué puede comprarse y bajo qué condiciones. Inventario trabaja con unidades disponibles y reservas. Facturación necesita describir lo vendido y conservar los importes y datos fiscales del documento emitido.

Al principio todo cabe en una clase Product. Después aparecen campos opcionales, validaciones condicionales y métodos que preguntan quién los está llamando. Cambiar el nombre comercial de la mochila acaba modificando lo que muestra una factura histórica. El problema ya no es que falte una carpeta: varias interpretaciones del negocio están compitiendo dentro del mismo modelo.

En este artículo vamos a separar esas interpretaciones. Mantendremos catálogo, ventas, inventario y facturación dentro de una aplicación Node.js, sin introducir microservicios. El resultado será una propuesta de límites, modelos pequeños y un contrato explícito que permita colaborar sin compartir todas las clases.

Qué delimita un Bounded Context

Un Bounded Context, o contexto delimitado, establece dónde es válido un modelo y qué significado tienen sus términos. Dentro de ese límite, las personas y el código deben poder usar el lenguaje del modelo sin contradicciones. Fuera de él, una palabra igual puede tener otro significado. Esa delimitación y la relación explícita entre modelos forman parte del diseño estratégico de DDD. Martin Fowler: Bounded Context.

Para nuestra tienda, «disponible» necesita un apellido:

  • Catálogo: visible para visitantes.
  • Ventas: habilitado para aceptar nuevas compras.
  • Inventario: con unidades que pueden reservarse.
  • Facturación: no es un estado relevante del producto; interesa la operación documentada.

Una mochila puede estar publicada, temporalmente bloqueada para nuevas ventas y todavía tener unidades físicas. Ninguna de esas situaciones invalida por sí misma las facturas anteriores.

Un contexto no se define contando tablas o pantallas. Se reconoce porque ofrece una interpretación coherente de un conjunto de decisiones. Los agregados que ya conoces protegen reglas dentro de ese modelo; no convierten automáticamente cada agregado en un contexto distinto.

Tampoco es una burbuja incomunicada. Separar modelos exige decidir qué información cruza el límite, con qué significado y quién responde por ella.

Domain, Subdomain, Bounded Context y Module

Estas cuatro preguntas ayudan a distinguirlos:

Domain: ¿qué actividad o problema estamos abordando? En el ejemplo, operar un comercio electrónico. El alcance depende del proyecto: la misma organización puede estar estudiando solamente su operación de devoluciones.

Subdomain: ¿qué parte del problema del negocio estamos analizando? Gestionar existencias o emitir documentos de facturación son capacidades que podemos estudiar dentro de ese dominio. No aparecen porque alguien haya creado una carpeta.

Bounded Context: ¿dónde se aplica un modelo determinado? Nuestro contexto de inventario decide cómo representar existencias y reservas. Es una decisión de modelado y responsabilidad, informada por el negocio, las personas y el sistema existente.

Module: ¿cómo organizamos conceptos dentro de ese modelo? Inventario puede contener módulos de reservas y ajustes. Compartirían el significado de sus conceptos, aunque agrupen comportamientos diferentes.

La distinción práctica es entre el problema que observamos y el modelo con el que lo abordamos. No fuerces una correspondencia permanente de un subdominio con un contexto: un sistema heredado puede mezclar capacidades que quieres separar, y una misma capacidad puede aparecer representada en varios sistemas.

Por eso src/modules/product no demuestra que exista un contexto de productos. Quizá solo concentra operaciones de cuatro modelos incompatibles. Del mismo modo, dos directorios no prueban que sus modelos sean independientes si ambos modifican la misma entidad global.

Cómo proponer límites con evidencia

Antes de escribir interfaces, reúne a quienes toman las decisiones del proceso. Como ejercicio de diseño para esta tienda, trabajaremos con cuatro pasos.

1. Recorrer una operación concreta

Una compradora abre la ficha de la mochila, confirma una compra, obtiene una reserva de unidades y solicita su factura. Anota qué decisión ocurre en cada paso y quién puede explicar sus reglas.

Evita empezar con «necesitamos cuatro servicios». Todavía no sabemos si cuatro modelos tienen sentido.

2. Buscar cambios de significado y de autoridad

Pregunta qué ocurre cuando alguien cambia una descripción, cancela un pedido o corrige su dirección. En nuestro ejemplo acordamos:

  • Catálogo mantiene la presentación actual del producto.
  • Ventas conserva las condiciones aceptadas en el pedido.
  • Inventario decide si puede reservar una cantidad.
  • Facturación conserva la representación del documento emitido según sus reglas.

Estas son decisiones del ejemplo, no una división universal de todas las tiendas.

3. Probar situaciones incómodas

Una factura emitida no debería cambiar porque catálogo actualice el título. Una reserva no debería aprobarse solo porque la ficha sea visible. Si necesitas consultar el estado interno de todos los demás modelos para explicar cada decisión, revisa el límite o el contrato.

También pregunta si dos responsabilidades comparten una regla que exige resolverse de manera atómica. No la dividas sin diseñar cómo se preservará. La independencia conceptual no resuelve por sí sola la coordinación de una operación.

4. Escribir una ficha breve por contexto

Por ejemplo:

Inventario. Decide sobre existencias reservables. Usa SKU, ubicación, cantidad y reserva. No decide precios ni redacta descripciones comerciales. Expone una solicitud de reserva y su resultado. Las reglas pertenecen al equipo responsable de operaciones de almacén.

La ficha permite discutir el límite antes de invertir en infraestructura. Revísala con casos reales y excepciones; un organigrama o una lista de sustantivos no bastan para validarla.

La documentación de Microsoft propone un análisis iterativo de capacidades y modelos, y muestra cómo distintas representaciones pueden servir a necesidades diferentes. Es un apoyo para la conversación, no un algoritmo que descubra los límites automáticamente. Análisis del dominio.

Caso principal: separar modelos sin perder la colaboración

El modelo global que introduce ambigüedad

Este fragmento es deliberadamente problemático:

interface Product {
  id: string;
  name: string;
  visible: boolean;
  stock: number;
  currentPriceCents: number;
  invoiceDescription?: string;
}

function invoiceLine(product: Product) {
  return {
    description: product.name,
    unitPriceCents: product.currentPriceCents,
  };
}

La función toma el precio actual para describir una venta anterior. Además, el único name obliga a decidir si es el título publicitario o la descripción del documento.

Agregar más propiedades opcionales puede ocultar el conflicto, pero no aclara quién decide cada valor ni cuándo queda fijado.

Cuatro representaciones con propósitos diferentes

El siguiente bloque es un fragmento de diseño: define tipos independientes; no implementa persistencia ni el flujo completo de compra.

// Catálogo: presentación actual.
interface CatalogProduct {
  readonly productId: string;
  readonly title: string;
  readonly description: string;
  readonly publication: "draft" | "published";
}

// Ventas: condiciones aceptadas en una línea del pedido.
interface SalesOrderLine {
  readonly productId: string;
  readonly titleAtPurchase: string;
  readonly quantity: number;
  readonly unitPriceCents: number;
  readonly currency: string;
}

// Inventario: unidad identificable para controlar existencias.
interface InventoryItem {
  readonly sku: string;
  readonly onHand: number;
  readonly reserved: number;
}

// Facturación: representación del documento emitido.
interface InvoiceLine {
  readonly description: string;
  readonly quantity: number;
  readonly unitPriceCents: number;
  readonly currency: string;
}

No son cuatro copias que deban sincronizar todos sus campos. Cada una responde una pregunta distinta. Ventas guarda lo aceptado; catálogo sigue evolucionando. Inventario no necesita el precio para decidir cuántas unidades puede reservar.

Tampoco supongas que productId y SKU son intercambiables. Una variante de talla puede tener su propio SKU; un paquete comercial podría necesitar varias unidades diferentes. Esa correspondencia merece un acuerdo explícito.

Customer y Order también cambian de significado

En ventas, Customer podría representar a quien realiza compras y sus condiciones comerciales. En facturación, un destinatario del documento requiere los datos pertinentes para su emisión. Compartir identificador no obliga a compartir clase ni todos los datos personales.

La misma cautela aplica a Order. Un pedido de ventas expresa compromisos comerciales. Inventario puede trabajar con una reserva vinculada a su referencia; facturación puede emitir uno o varios documentos relacionados con él. No necesita importar el agregado de ventas para comprender esos hechos.

Cuando falta información, pregunta qué decisión necesita el consumidor. Enviar el objeto completo «por si acaso» acopla a su estructura interna y puede exponer datos innecesarios.

El límite se ve en un contrato

Supongamos una necesidad acotada: preparar una vista previa de facturación a partir de las condiciones confirmadas en ventas. No estamos calculando impuestos ni emitiendo un documento fiscal.

Ventas ofrece una representación pública como esta:

{
  "orderId": "order-100",
  "currency": "EUR",
  "lines": [
    {
      "description": "Mochila urbana",
      "quantity": 2,
      "unitPriceCents": 3500
    }
  ]
}

En este contrato los importes en EUR se expresan en céntimos: 3500 representa 35 euros. En un sistema real define explícitamente unidades y redondeos por moneda; no infieras que todas tienen dos decimales operativos.

El contrato promete condiciones de una venta confirmada. No promete la estructura de Order, el precio vigente del catálogo ni cómo se almacena el pedido.

Facturación define su propio puerto de lectura. Este bloque es autocontenido como ejemplo de TypeScript; usa un doble en memoria y aserciones propias, sin librerías externas. No realiza peticiones HTTP ni constituye una integración lista para producción.

interface ConfirmedSaleSnapshot {
  readonly orderId: string;
  readonly currency: string;
  readonly lines: readonly {
    readonly description: string;
    readonly quantity: number;
    readonly unitPriceCents: number;
  }[];
}

interface ConfirmedSalesReader {
  findConfirmed(orderId: string): Promise<ConfirmedSaleSnapshot>;
}

interface BillingPreview {
  readonly sourceOrderId: string;
  readonly currency: string;
  readonly totalCents: number;
  readonly lines: readonly {
    readonly description: string;
    readonly quantity: number;
    readonly unitPriceCents: number;
  }[];
}

async function prepareBillingPreview(
  orderId: string,
  sales: ConfirmedSalesReader,
): Promise<BillingPreview> {
  const sale = await sales.findConfirmed(orderId);
  if (sale.orderId !== orderId) {
    throw new Error("La respuesta no corresponde al pedido solicitado");
  }

  return {
    sourceOrderId: sale.orderId,
    currency: sale.currency,
    lines: sale.lines.map((line) => ({ ...line })),
    totalCents: sale.lines.reduce(
      (total, line) => total + line.quantity * line.unitPriceCents,
      0,
    ),
  };
}

async function demonstrateBoundary(): Promise<void> {
  const reader: ConfirmedSalesReader = {
    async findConfirmed(orderId) {
      return {
        orderId,
        currency: "EUR",
        lines: [{
          description: "Mochila urbana",
          quantity: 2,
          unitPriceCents: 3500,
        }],
      };
    },
  };

  const preview = await prepareBillingPreview("order-100", reader);
  if (preview.totalCents !== 7000) {
    throw new Error("Debe utilizar el precio aceptado en la venta");
  }
  if (preview.lines[0]?.description !== "Mochila urbana") {
    throw new Error("Debe conservar la descripción del contrato");
  }
}

void demonstrateBoundary();

La decisión relevante es qué conoce prepareBillingPreview: una representación del consumidor, sin importar clases internas de ventas. El doble demuestra qué necesita para funcionar y qué resultado esperamos; no prueba compatibilidad HTTP.

En una implementación real, un adaptador HTTP traduce y valida la respuesta externa antes de devolver ConfirmedSaleSnapshot. Debe comprobar estructura, identidad, cantidades, moneda e importes, además de manejar errores. Esa traducción es un primer ejemplo de protección del modelo consumidor.

Aquí asumimos importes enteros seguros y entradas válidas proporcionadas por el doble. Antes de producción hacen falta límites numéricos, validación del contrato y pruebas del adaptador. No hemos implementado emisión fiscal, impuestos, persistencia ni reserva de existencias.

Una aplicación, varios contextos

Podemos organizar el monolito así:

src/contexts/
  catalog/
    domain/
    application/
    infrastructure/
  sales/
    domain/
    application/
    infrastructure/
  inventory/
    domain/
    application/
    infrastructure/
  billing/
    domain/
    application/
    infrastructure/
      sales-http-reader.ts

El directorio comunica una intención; la independencia se verifica en las dependencias. En esta propuesta, facturación usa su puerto y un adaptador HTTP con un contrato del consumidor. No importa el repositorio, el agregado ni las entidades internas de ventas. Ese HTTP puede dirigirse a la misma aplicación.

Es una elección deliberada del ejemplo para hacer visible el contrato. No significa que todo monolito necesite llamadas HTTP internas. Otros diseños usan interfaces públicas en proceso, siempre que la colaboración no permita saltarse las reglas del otro modelo.

Un Bounded Context puede vivir dentro de un monolito modular. Extraerlo a un servicio añade decisiones de operación y comunicación que deben justificarse. Tener cuatro procesos con la misma clase global y acceso indiscriminado a las mismas tablas tampoco produce cuatro modelos independientes.

Si comparten una instancia de base de datos, acuerda la propiedad de los datos: facturación no debería actualizar directamente las tablas de ventas. Una llamada HTTP no participa automáticamente en la transacción local del consumidor.

Para nuestra vista previa solo leemos una venta confirmada. Si después quisiéramos coordinar cobro, reserva y emisión, tendríamos que diseñar sus fallos y compromisos por separado. El contrato no garantiza que todo ocurra o se revierta conjuntamente.

Del Domain Event al contrato de integración

Ventas puede registrar un OrderConfirmed interno. Su estructura puede ser útil para el propio modelo sin ser adecuada para consumidores externos.

Si facturación necesita conocer ese hecho, define qué información necesita y acuerda un contrato de integración. Puede contener identificadores y condiciones aceptadas, con una versión y significado explícitos. Traducir el evento evita publicar accidentalmente todos los detalles internos del agregado.

Un cambio interno no debería obligar automáticamente a todos los consumidores a modificar su código. A la inversa, un contrato público no es inmutable para siempre: requiere compatibilidad y coordinación al evolucionar.

Aquí interesa reconocer la frontera. El transporte, la publicación y la recuperación ante fallos pertenecen a otro nivel de diseño.

Un Context Map que permita conversar

Un Context Map muestra contextos, sus relaciones y los acuerdos que condicionan su colaboración. No es solamente un dibujo de peticiones de red. Puede reflejar quién provee información y quién debe adaptarse.

Para nuestra propuesta:

Catálogo ---- oferta comercial ----> Ventas
Ventas ------ solicitud -----------> Inventario
Ventas <----- resultado de reserva - Inventario
Ventas ------ venta confirmada ----> Facturación

Cada flecha representa un contrato por discutir. En la primera hay que precisar si el precio proviene de catálogo o de otro responsable. En la segunda, cómo se relacionan las líneas comerciales con los SKU. En la última, qué condiciones quedan fijadas para facturación.

Las flechas muestran información, no una secuencia temporal completa. En cada relación conviene anotar propietario, significado y capacidad de negociación.

Estos nombres permiten introducir alternativas sin implementarlas todavía:

  • Anti-Corruption Layer: traducción que protege el modelo consumidor de conceptos externos.
  • Shared Kernel: pequeña parte del modelo compartida deliberadamente, con cambios coordinados.
  • Customer/Supplier: relación donde el equipo consumidor participa en la planificación del proveedor.
  • Published Language: lenguaje de intercambio documentado que ambas partes pueden interpretar.

No son cuatro tipos excluyentes de flecha: describen aspectos distintos y pueden combinarse. Un proveedor puede ofrecer un lenguaje publicado y el consumidor mantener su traducción. Compartir un paquete de utilidades no equivale, por sí solo, a compartir un modelo. Eric Evans: DDD Reference, sección Context Mapping.

Dos situaciones que cambian la decisión

Educación: estudiante inscrito y persona evaluada

Una plataforma educativa necesita habilitar acceso a un curso y registrar calificaciones. Si las reglas académicas y sus responsables difieren de las de inscripción, podemos proponer modelos separados: una matrícula habilita participación; un expediente registra resultados.

La integración transmite la referencia de la matrícula y el curso, no una entidad Student con acceso libre a todas las notas. Este ejemplo enseña a separar por decisiones y confidencialidad, no por nombres de personas.

Pero si la plataforma apenas lista cursos y registra inscripciones, crear un contexto de evaluación sin esa necesidad sería anticipar complejidad.

Reservas: cuando dividir empeora una regla

Una pequeña aplicación reserva salas por franja horaria. Si disponibilidad y reserva son dos caras de la misma regla —no aceptar dos reservas solapadas—, empezar con un único modelo puede ser lo más claro.

Separarlas en contextos solo porque existen dos pantallas obliga a resolver coordinación adicional. La lección es conservar una decisión unificada cuando todavía no existe evidencia de modelos distintos.

Si más adelante aparece un sistema externo de planificación con otra interpretación de disponibilidad, ese nuevo problema sí puede justificar una frontera y una traducción.

Cómo evitar que la IA borre los límites

Dar a una IA una carpeta llamada billing no le explica qué significa facturar. Indica qué decisiones pertenecen al contexto, qué contratos consume y qué conceptos no debe importar.

Puedes usar estos prompts como puntos de partida:

Para analizar límites:

Analiza este flujo de compra y estos ejemplos de excepciones. Propón contextos según vocabulario, reglas y responsables. Distingue hechos observados de hipótesis. Entrega una ficha por contexto y tres preguntas para validar los límites con el negocio. No propongas microservicios ni conviertas cada entidad en un contexto.

Para implementar una colaboración:

Trabaja únicamente en facturación. Consume la venta confirmada mediante el puerto del consumidor y un adaptador HTTP. No importes entidades ni repositorios de ventas. Define unidades monetarias y errores del contrato. Entrega fragmentos pequeños y separa validación externa de decisiones locales; no implementes emisión fiscal.

Para revisar:

Revisa este diff buscando modelos globales, importaciones entre contextos, escritura en datos ajenos y confusión entre precio actual y precio aceptado. Señala la evidencia concreta y el comportamiento afectado. Propón la corrección mínima; no recomiendes distribuir servicios sin justificarlo.

Una persona debe verificar después que el contrato representa el acuerdo real, que la información compartida es necesaria y que las dependencias respetan el límite. El código generado no sustituye esa conversación.

Ejercicios para aplicar los límites

1. Cambiar la descripción

Catálogo renombra «Mochila urbana» como «Mochila urbana 2027». Una factura anterior cambia también porque consulta el título actual. ¿Qué revisarías?

2. El almacén de productos

Un equipo propone un contexto diferente por cada entidad: Product, Customer y Order. ¿Qué información falta antes de aceptar esa división?

3. La integración rápida

Facturación importa SalesOrderRepository para calcular una vista previa. Otro desarrollador propone reemplazar el import por HTTP, enviando el agregado completo serializado. ¿La segunda solución resuelve el problema?

Soluciones razonadas

1. Revisaría qué representación conserva el documento y cuál fue la descripción aceptada para esa operación. La factura no debe depender accidentalmente de una presentación que cambia. Eso puede requerir conservar datos históricos en el modelo propietario y traducir las condiciones de la venta al preparar el documento; no obliga a copiar todo el catálogo.

2. Faltan decisiones, significados, reglas compartidas y responsables. Las entidades pueden colaborar dentro de un mismo contexto. Probaría el flujo de compra y sus excepciones para descubrir dónde cambia realmente el modelo, incluyendo qué reglas necesitan mantenerse juntas.

3. HTTP evita el import directo, pero transportar todo el agregado mantiene una dependencia de su estructura. La corrección es definir qué necesita facturación, acordar el contrato y traducirlo al modelo consumidor. Además, hay que validar la respuesta y reconocer que la llamada no crea una transacción compartida.

Qué comprobar antes de adoptar un límite

Un límite útil permite explicar qué decide cada contexto, qué significan sus palabras y qué necesita de los demás. Prueba la propuesta con una operación normal y una excepción: ¿puedes cambiar una regla local sin reinterpretar todos los modelos?

Si la separación solo añade carpetas, contratos vacíos o pasos remotos alrededor de una misma decisión, simplifica. Si evita que presentación comercial, existencias y documentos históricos compitan dentro de una clase global, está aportando claridad.

El objetivo es conservar modelos coherentes y hacer explícita su colaboración. El despliegue puede seguir siendo uno solo mientras esa sea la opción adecuada.

Cargando tus opciones de aprendizaje…

Cargando conversación…

Sigue aprendiendo