Arquitectura de Software

Domain-Driven Design: Separate Ways, cuándo es mejor mantener los Bounded Contexts independientes.

Aprende a decidir cuándo no integrar dos contextos. Compara descuentos de Sales e incentivos de Marketing con TypeScript, evalúa costes y duplicación y documenta la independencia junto con sus límites y condiciones de revisión.

Cargando tus opciones de aprendizaje…

Dos descuentos que no representan la misma decisión

Sales necesita calcular una reducción comercial que se aplicará a un pedido. Marketing necesita estimar cuánto presupuesto asignar a incentivos durante la planificación de una campaña.

En ambos equipos aparece una función con números, porcentajes y límites. Al revisar el código, alguien propone crear un servicio global de descuentos: «Si hacen lo mismo, compartamos la implementación».

Pero no hacen lo mismo. Sales decide cuánto puede descontarse de una venta real. Marketing compara escenarios internos que todavía no son ofertas para clientes.

Unificar esas reglas puede hacer que un ajuste de campaña cambie el importe de pedidos o que una nueva condición comercial bloquee una simulación. En este caso, aceptar cierta similitud de código puede resultar menos costoso que mantener una dependencia artificial.

Ese es el tipo de decisión que vamos a estudiar con Separate Ways, o caminos separados.

Qué significa elegir caminos separados

Separate Ways propone no establecer una integración cuando la relación entre las capacidades no es suficientemente importante para justificarla. Los contextos resuelven sus necesidades de manera independiente, con soluciones ajustadas a su alcance.

Evans señala que integrar tiene un coste y que el beneficio puede ser pequeño; cuando las funcionalidades no tienen una relación significativa, mantenerlas separadas permite soluciones especializadas. DDD Reference: Separate Ways, página 37 del documento.

No es una licencia para ignorar procesos compartidos. Es una decisión basada en que el intercambio o la coordinación no son necesarios para la capacidad analizada.

En nuestro ejemplo, el alcance será el cálculo del descuento del pedido frente a la estimación interna de incentivos. No afirmamos que toda la organización de Sales y Marketing carezca de relación.

Sales                         Marketing
Descuento de pedido            Estimación de incentivos
Reglas comerciales             Reglas de planificación
Pruebas locales                Pruebas locales

Sin servicio global ni intercambio entre estos cálculos.

La ausencia de una flecha es deliberada. Si descubrimos que los resultados de Marketing deben aplicarse en Sales, el mapa tendrá que reflejar esa dependencia.

Independencia no significa dos servidores

Los módulos pueden vivir en la misma aplicación Node.js o en el mismo repositorio. No necesitan desplegarse como microservicios para mantener modelos y reglas independientes.

También pueden compartir infraestructura técnica, como herramientas de compilación, sin compartir por ello una política de descuentos.

Lo que revisamos aquí es la dependencia entre modelos y decisiones del negocio. Compartir un proceso no demuestra que exista una integración de dominio; tener procesos separados tampoco demuestra autonomía conceptual.

La abstracción global que empieza a crecer

Este fragmento es un contraejemplo didáctico:

interface DiscountInput {
  context: "sales" | "marketing";
  baseAmount: number;
  percentage: number;
  maximum: number;
}

function calculateGlobalDiscount(input: DiscountInput): number {
  if (input.context === "sales") {
    return Math.min(
      Math.floor(input.baseAmount * input.percentage / 100),
      input.maximum,
    );
  }

  return Math.min(input.baseAmount, input.maximum);
}

El servicio necesita saber quién lo llama. baseAmount puede significar subtotal de pedido o presupuesto por persona, y maximum puede representar límites con unidades distintas.

La generalización no ha encontrado una regla común. Ha colocado dos decisiones bajo una firma poco precisa.

A medida que cambien las necesidades, aparecerán parámetros como campaignMode, customerSegment o applyOrderMinimum. Cada equipo tendrá que comprobar que sus cambios no afectan una rama ajena.

El problema no es tener un condicional. Es crear un punto de evolución conjunta sin una responsabilidad de negocio que lo justifique.

Comprobar si la independencia es posible

Antes de separar, contrastamos estos supuestos de la tienda:

  • Sales puede calcular el descuento del pedido sin leer campañas.
  • Marketing simula incentivos con un presupuesto y una audiencia introducidos para la planificación.
  • La simulación no genera cupones canjeables ni promete descuentos al cliente.
  • Ninguno de los cálculos exige que el otro produzca el mismo resultado.
  • Los responsables aceptan que sus reglas evolucionen por motivos distintos.

Si el tercer punto no se cumple, el caso cambia de manera importante. Una campaña anunciada al cliente puede comprometer el comportamiento de Sales, aunque hoy no exista una API.

La decisión Separate Ways no puede esconder esa obligación detrás de una hoja de cálculo o un intercambio manual.

Modelo local de Sales: descuento sobre un pedido

Para este ejemplo, Sales acuerda una regla concreta:

En pedidos de al menos 100 euros, aplicar un 10 % de descuento con un máximo de 20 euros. Redondear el descuento hacia abajo al céntimo.

No es una recomendación comercial universal. Es una política inventada para mostrar dónde pertenece la regla.

En sales/order-discount.ts:

export interface OrderDiscount {
  readonly discountCents: number;
  readonly payableCents: number;
}

export function calculateOrderDiscount(
  subtotalCents: number,
): OrderDiscount {
  if (
    !Number.isSafeInteger(subtotalCents) ||
    subtotalCents < 0 ||
    subtotalCents > 1_000_000_000
  ) {
    throw new Error("Subtotal inválido para esta operación");
  }

  const discountCents = subtotalCents >= 10_000
    ? Math.min(Math.floor(subtotalCents / 10), 2_000)
    : 0;

  return {
    discountCents,
    payableCents: subtotalCents - discountCents,
  };
}

El límite de entrada es una decisión del ejemplo para acotar el cálculo. La función trabaja únicamente en céntimos de EUR y no calcula impuestos ni acumula otras promociones.

Para un subtotal de 20 000 céntimos, devuelve 2000 de descuento y 18 000 pagables.

El resultado puede utilizarse al preparar un pedido, pero no implementa su aceptación ni persistencia. Sales debe conservar las condiciones aceptadas según su proceso, sin recalcular pedidos históricos con futuras versiones de la política.

Marketing no importa esta función ni necesita conocer el mínimo de compra para planificar su presupuesto.

Modelo local de Marketing: estimar un incentivo

Marketing está comparando escenarios internos. Su regla es:

Repartir un presupuesto entre una audiencia prevista, con un máximo de 10 euros por persona. El incentivo se expresa en céntimos enteros y no puede superar el presupuesto.

En marketing/campaign-incentive.ts:

export interface CampaignEstimate {
  readonly incentivePerPersonCents: number;
  readonly estimatedSpendCents: number;
  readonly unallocatedBudgetCents: number;
}

export function estimateCampaignIncentive(
  budgetCents: number,
  expectedAudience: number,
): CampaignEstimate {
  if (
    !Number.isSafeInteger(budgetCents) ||
    budgetCents < 0 ||
    budgetCents > 1_000_000_000
  ) {
    throw new Error("Presupuesto inválido");
  }

  if (
    !Number.isSafeInteger(expectedAudience) ||
    expectedAudience < 1 ||
    expectedAudience > 1_000_000
  ) {
    throw new Error("La audiencia debe ser un entero positivo válido");
  }

  const incentivePerPersonCents = Math.min(
    Math.floor(budgetCents / expectedAudience),
    1_000,
  );
  const estimatedSpendCents =
    incentivePerPersonCents * expectedAudience;

  return {
    incentivePerPersonCents,
    estimatedSpendCents,
    unallocatedBudgetCents: budgetCents - estimatedSpendCents,
  };
}

Con un presupuesto de 50 000 céntimos y 500 personas previstas, el incentivo estimado es de 100 céntimos por persona. Es una asignación de planificación, no el valor de un cupón emitido.

Si el presupuesto no alcanza un céntimo por persona, el resultado es cero. La función no garantiza conversiones, gasto real o respuesta de la audiencia.

Sales no necesita llamar a esta simulación para aplicar su descuento comercial. Marketing puede modificar su límite de planificación sin cambiar los importes de pedidos.

Esta independencia es la decisión importante; las funciones podrían compartir operaciones aritméticas y seguir expresando conocimientos diferentes.

Estructura de módulos y pruebas locales

Una organización sencilla:

src/
  sales/
    order-discount.ts
    order-discount.test.ts
  marketing/
    campaign-incentive.ts
    campaign-incentive.test.ts

Sin importaciones de reglas entre ambos módulos.
Sin paquete global de descuentos.

No hemos implementado endpoints ni almacenamiento porque ninguna operación requiere integrar ambos contextos. Añadirlos solo para demostrar arquitectura contradiría el objetivo.

Los siguientes son ejemplos de pruebas unitarias con APIs integradas de Node.js. Presuponen compilación de TypeScript a JavaScript ESM. No se han ejecutado como parte del artículo.

Las pruebas de Sales explican su política

import test from "node:test";
import assert from "node:assert/strict";
import { calculateOrderDiscount } from "./order-discount.js";

test("no descuenta por debajo del mínimo comercial", () => {
  assert.deepEqual(calculateOrderDiscount(9_999), {
    discountCents: 0,
    payableCents: 9_999,
  });
});

test("aplica el porcentaje desde el mínimo incluido", () => {
  assert.deepEqual(calculateOrderDiscount(10_000), {
    discountCents: 1_000,
    payableCents: 9_000,
  });
});

test("limita el descuento a veinte euros", () => {
  assert.deepEqual(calculateOrderDiscount(30_000), {
    discountCents: 2_000,
    payableCents: 28_000,
  });
});

test("rechaza fracciones de céntimo", () => {
  assert.throws(() => calculateOrderDiscount(100.5));
});

Las pruebas de Marketing explican su estimación

import test from "node:test";
import assert from "node:assert/strict";
import { estimateCampaignIncentive } from "./campaign-incentive.js";

test("reparte el presupuesto entre la audiencia prevista", () => {
  assert.deepEqual(estimateCampaignIncentive(50_000, 500), {
    incentivePerPersonCents: 100,
    estimatedSpendCents: 50_000,
    unallocatedBudgetCents: 0,
  });
});

test("conserva el presupuesto que excede el límite por persona", () => {
  assert.deepEqual(estimateCampaignIncentive(50_000, 10), {
    incentivePerPersonCents: 1_000,
    estimatedSpendCents: 10_000,
    unallocatedBudgetCents: 40_000,
  });
});

test("deja sin asignar los céntimos del reparto indivisible", () => {
  assert.deepEqual(estimateCampaignIncentive(101, 3), {
    incentivePerPersonCents: 33,
    estimatedSpendCents: 99,
    unallocatedBudgetCents: 2,
  });
});

test("no calcula una asignación con audiencia cero", () => {
  assert.throws(() => estimateCampaignIncentive(50_000, 0));
});

No necesitamos una prueba que obligue a ambos modelos a producir el mismo importe. No representan la misma decisión.

Sí conviene comprobar dependencias para evitar que, con el tiempo, un módulo empiece a consultar al otro por comodidad. La estructura de carpetas es una intención; las importaciones y los flujos efectivos muestran si se conserva.

Duplicación técnica y duplicación de conocimiento

Una operación repetida no demuestra que haya una regla compartida.

Duplicación técnica: ambos módulos validan enteros o realizan una división. Puede ser razonable aceptar unas líneas locales, o utilizar una utilidad técnica estable si ya existe y aporta valor.

Duplicación aparente de negocio: ambos hablan de «descuento», pero uno modifica un pedido y otro estima presupuesto. Compartir código puede ocultar que las decisiones son diferentes.

Duplicación del mismo conocimiento: ambos deben aplicar exactamente una política aprobada, con el mismo significado y cambios obligatoriamente coherentes. Aquí la independencia necesita reconsiderarse.

No conviertas Separate Ways en un argumento para copiar sin límite. Si una corrección importante debe repetirse siempre en varios lugares por la misma razón, investiga si realmente hay conocimiento compartido.

Tampoco uses «no duplicar código» como razón suficiente para crear una llamada remota. El coste de esa dependencia puede superar el de mantener dos funciones pequeñas.

Evaluar el coste completo de integrar

Antes de extraer un servicio común, compara dos alternativas concretas: mantener las reglas locales o introducir una dependencia compartida.

Beneficio de negocio

¿Qué error o trabajo se elimina? ¿Qué capacidad nueva aparece? En nuestro ejemplo no necesitamos que ambas funciones produzcan lo mismo, así que el beneficio de una regla única es dudoso.

Desarrollo y mantenimiento

La integración requiere un contrato, compatibilidad, pruebas de los consumidores y responsables de sus cambios. Una biblioteca compartida evita parte del transporte, pero mantiene coordinación sobre su comportamiento.

La independencia también cuesta: pruebas separadas, revisión de cada política y posible repetición de mejoras técnicas. No la presentes como una opción gratuita.

Disponibilidad y rendimiento

Si el cálculo del pedido consulta un servicio de campañas, Sales puede depender de su disponibilidad para vender. Hay que justificar qué obtiene a cambio.

Las funciones locales del ejemplo no añaden esa dependencia de ejecución, aunque sigan dependiendo de su propio entorno.

Evolución y coordinación

¿Puede Marketing probar otra estrategia sin pedir cambios a Sales? ¿Puede Sales modificar el descuento mínimo sin revisar simulaciones?

La autonomía aporta valor cuando esas decisiones pertenecen realmente a responsables distintos. Si ambos deben mantener una misma promesa al cliente, coordinarlas deja de ser opcional.

No hace falta inventar porcentajes de ahorro. Si no hay mediciones, identifica costes esperados, incertidumbres y evidencia que permitiría comparar las alternativas.

Documentar la decisión sin convertirla en una prohibición eterna

Este es un ejemplo breve de decisión arquitectónica, no una decisión observada en un repositorio real.

Contexto

Sales aplica descuentos sobre pedidos. Marketing realiza simulaciones internas de presupuesto. No existe una campaña que emita beneficios canjeables mediante estas funciones.

Las políticas tienen objetivos y responsables distintos.

Decisión

Mantener cálculos y pruebas independientes. No crear un servicio global de descuentos ni importar reglas entre Sales y Marketing para estas capacidades.

Alternativas consideradas

Paquete común de descuentos: reduciría algunas líneas repetidas, pero impondría un punto de evolución conjunta a reglas diferentes.

Servicio remoto de campañas: añadiría disponibilidad, contratos y coordinación sin una necesidad actual de intercambio.

Consecuencias

Cada equipo puede evolucionar su cálculo dentro de su responsabilidad. Se acepta cierta duplicación técnica y la obligación de mantener pruebas locales.

No se promete coherencia entre una estimación de Marketing y un descuento de Sales porque no representan una oferta compartida.

Condiciones de revisión

Revisar si Marketing empieza a emitir cupones, si una campaña anuncia descuentos que Sales debe respetar o si aparece una política monetaria que ambos deban aplicar exactamente.

La decisión debe tener responsables y revisarse ante esas condiciones. No necesita una fecha inventada para parecer completa.

Cuando Separate Ways deja de ser apropiado

Una promoción anunciada debe cumplirse

Si Marketing promete un 15 % en compras elegibles y Sales debe aplicarlo, existe una relación de negocio. No puede ignorarse porque los equipos prefieran independencia.

Hay que establecer quién define la promoción, cómo se interpreta y cómo se verifica su aplicación. El mecanismo concreto se decide después; la necesidad de coordinación ya existe.

Hay datos maestros o decisiones con autoridad común

Si ambos procesos necesitan utilizar la misma referencia oficial de cliente, producto o condición comercial, identifica al responsable y las garantías necesarias.

Tener copias locales puede ser una estrategia técnica, pero si deben alimentarse y mantenerse desde una fuente común, hay una integración que debe documentarse.

Existen obligaciones de cumplimiento

Una obligación aplicable puede exigir consistencia, trazabilidad o coordinación entre procesos. Primero se determina el requisito real con sus responsables; no se asume que el patrón permite evitarlo.

Separate Ways no significa que cada equipo invente por separado el significado de una obligación compartida.

Hay intercambio manual que sostiene el proceso

Una persona que copia reglas desde campañas al sistema de ventas mantiene una conexión operativa aunque no exista API.

El mapa debe reflejarla. No describas como independencia completa lo que depende de sincronización humana para cumplir una promesa.

Compararlo con las alternativas sin mezclar decisiones

Shared Kernel: puede servir cuando existe una parte pequeña del modelo con significado común y gobierno conjunto. Aquí las reglas son diferentes y no justifican ese compromiso.

Partnership: corresponde a una dependencia de éxito que requiere colaboración estrecha. No hace falta crear esa relación entre dos cálculos que pueden cumplir sus objetivos por separado.

Customer/Supplier: establece una dependencia negociada entre proveedor y consumidor. Solo tiene sentido si hay una capacidad que deba consumirse.

Anti-Corruption Layer: protege un modelo cuando la integración existe y hay que traducir. Separate Ways puede evitar esa integración si no tiene un beneficio suficiente; no reemplaza una traducción cuando el intercambio es necesario.

Published Language: define cómo interpretar información compartida. Si no necesitamos intercambiarla, no hay motivo para diseñar un mensaje solo para conectar nombres parecidos.

La decisión no es encontrar el patrón más elaborado. Es reconocer qué relación necesita realmente el negocio.

Dos ejemplos fuera del descuento comercial

Educación: estrellas y calificaciones

Un juego educativo muestra estrellas por completar retos. Un expediente académico registra notas con reglas institucionales.

Aunque ambos usan números y hablan de rendimiento, no hay razón para compartir una calculadora de puntuación si ninguna capacidad requiere equivalencia entre esos resultados.

Si más adelante los retos otorgan créditos académicos, habrá que establecer criterios y responsabilidad. El nuevo requisito cambia la relación; no justifica haberla construido antes por intuición.

Reservas: la independencia que sería falsa

Un sistema de salas acepta reservas y otro permite bloquear esos mismos espacios para mantenimiento. Si ambos deciden sobre las mismas franjas físicas, pueden generar conflictos reales.

No basta con crear dos calendarios locales y afirmar que siguen caminos separados. Hay una necesidad de coordinación sobre disponibilidad.

Este contraejemplo enseña a buscar el recurso o la promesa compartidos. La autonomía técnica no elimina una colisión del negocio.

Pedir a la IA que cuestione la reutilización automática

Cursor, Claude o Codex pueden detectar dos funciones parecidas y proponer shared/discounts. Eso no demuestra que la abstracción sea correcta.

Puedes usar estos prompts:

Para analizar:

Compara el descuento aplicado a pedidos de Sales y la estimación interna de incentivos de Marketing. Identifica objetivos, unidades, reglas y razones de cambio. Distingue similitud técnica de conocimiento compartido. No propongas una integración sin una necesidad de negocio concreta.

Para implementar:

Mantén funciones y pruebas locales en ambos contextos. Sales aplica su política comercial; Marketing solo simula presupuesto y no emite beneficios canjeables. No añadas imports entre contextos, paquetes globales ni llamadas remotas para eliminar unas líneas repetidas.

Para revisar una nueva necesidad:

Esta campaña ahora promete beneficios que Sales debe aplicar. Contrasta el requisito con la decisión Separate Ways anterior. Identifica qué supuesto dejó de cumplirse y qué acuerdos hacen falta. No inventes una arquitectura completa ni afirmes que la independencia sigue siendo válida sin cambios.

Una persona debe confirmar si hay una promesa al cliente, un recurso compartido o una obligación común. El código no contiene siempre toda esa información.

Ejercicios

1. Dos fórmulas idénticas hoy

Sales y Marketing usan temporalmente un 10 %. Un equipo propone una única función porque los números coinciden. ¿Qué información falta?

2. Un cupón nuevo

Marketing empieza a enviar códigos que Sales debe aceptar bajo ciertas condiciones. ¿Puede conservarse la decisión anterior sin revisión?

3. La regla común oculta

Ambos contextos reciben por correo una política oficial que debe aplicarse sin diferencias. Cada equipo la transcribe en su código. ¿Es un buen caso de independencia completa?

Soluciones razonadas

1. Falta saber qué decisión representa cada fórmula, quién la mantiene y si debe cambiar por la misma razón. Coincidir en un porcentaje no demuestra un modelo común.

2. No. Aparece una relación operativa entre la promesa de Marketing y la aceptación de Sales. Hay que definir autoridad, condiciones y coordinación, y actualizar el mapa y la decisión.

3. No necesariamente. Existe conocimiento compartido cuya coherencia importa y una conexión mediante el proceso de distribución de la política. Debe analizarse cómo garantizarla; duplicar la transcripción puede introducir divergencias.

Mantener caminos separados con razones explícitas

Separate Ways aporta valor cuando cada contexto puede cumplir su responsabilidad sin una integración cuyo coste no compensa.

Comprueba que la independencia no oculta promesas, recursos o reglas comunes; que los cálculos tienen motivos de cambio distintos; y que sabes cuándo revisar la decisión.

Aceptar dos soluciones pequeñas puede ser más claro que sostener una abstracción global. La independencia es útil mientras respeta las necesidades reales del negocio.

Cargando tus opciones de aprendizaje…

Cargando conversación…

Sigue aprendiendo