Arquitectura de Software

Domain-Driven Design: Customer/Supplier, cómo coordinar dependencias y acuerdos entre Bounded Contexts.

Aprende a coordinar Inventory y Sales mediante compromisos entre equipos, contratos explícitos y validación en ejecución. Diseña un consumidor en TypeScript, distingue pruebas locales de verificación del proveedor y planifica cambios compatibles.

Cargando tus opciones de aprendizaje…

Cuando el proveedor entrega y el consumidor sigue bloqueado

Inventory publica una nueva API de disponibilidad. Sus pruebas pasan y su equipo considera terminada la entrega. Sales intenta utilizarla para vender dos mochilas, pero descubre que «disponible» incluye unidades que no pueden reservarse por estar retenidas para revisión.

Nadie ha roto el formato del JSON. Sin embargo, los equipos no habían acordado qué necesitaba Sales para tomar su decisión.

El patrón Customer/Supplier Development, o Cliente/Proveedor, trata precisamente de coordinar una dependencia entre equipos. La API es una parte del resultado; también importan las prioridades, las responsabilidades y los compromisos que la hacen útil.

Vamos a trabajar con Inventory como proveedor y Sales como consumidor. Construiremos un contrato pequeño de consulta, un adaptador de Sales y ejemplos de verificación. Después veremos cómo negociar su evolución sin convertir cada cambio interno en una aprobación entre departamentos.

Una dependencia con colaboración real

Inventory es Upstream respecto de la capacidad de disponibilidad que ofrece. Sales es Downstream porque su proceso depende de ese contrato y de su significado.

Inventory [Upstream / Supplier]
            |
            | Contrato de disponibilidad acordado
            v
Sales [Downstream / Customer]

La flecha muestra dependencia del consumidor respecto del proveedor. Sales puede iniciar la petición HTTP y seguir siendo Downstream. «Customer» describe aquí al equipo consumidor, no al comprador de la tienda.

La característica decisiva del patrón es que las necesidades Downstream influyen en la planificación Upstream. Ambos negocian el trabajo, sus expectativas y cuándo podrán contar con lo acordado. Evans también propone pruebas de aceptación conjuntas para verificar la interfaz esperada. DDD Reference: Customer/Supplier Development, página 32 del documento.

Esto evita dos extremos: un proveedor que ignora al consumidor y un consumidor que pretende vetar cualquier evolución del proveedor.

Una API publicada, un canal de chat o una reunión mensual no demuestran por sí solos que exista Customer/Supplier. Necesitas evidencia de que una necesidad se convierte en un compromiso y de que ambos pueden verificar su cumplimiento.

Negociar una necesidad antes de diseñar el DTO

Sales plantea inicialmente: «necesitamos el stock de cada producto». Inventory pregunta para qué.

La necesidad real es mostrar si una cantidad concreta parece reservable antes de iniciar la reserva. Sales no necesita conocer ubicaciones, bloqueos internos o movimientos del almacén.

Los equipos acuerdan este alcance:

  • Sales identifica el SKU y la cantidad solicitada.
  • Inventory informa cuántas unidades son reservables en el momento de la consulta.
  • Una consulta no retiene unidades ni garantiza una reserva posterior.
  • Un SKU desconocido produce una respuesta distinta de una disponibilidad igual a cero.
  • Un fallo técnico no se interpreta como «sin existencias».
  • Inventory conserva la decisión sobre sus reglas internas de disponibilidad.

Es un acuerdo del ejemplo. Otra tienda podría preferir que Inventory evaluara directamente una cantidad y devolviera una decisión. Lo importante es elegir deliberadamente y documentar la semántica.

Repartir responsabilidades

Inventory implementa la consulta con sus reglas, valida los parámetros y conserva el significado del campo público. No expone su entidad de almacén ni obliga al consumidor a reconstruir sus cálculos.

Sales valida su entrada comercial, comprueba las respuestas y mantiene la distinción entre una estimación consultada y una reserva aceptada.

Ambos equipos acuerdan ejemplos, compatibilidad, fecha de entrega y criterios de retirada de versiones.

El proveedor puede cambiar cómo calcula la disponibilidad sin pedir permiso para cada refactor. Debe coordinar un cambio cuando altera lo que el consumidor puede esperar del contrato.

El contrato HTTP que ambos pueden revisar

Proponemos esta consulta para una tienda con SKU sencillo:

GET /api/v1/availability?sku=BAG-URBAN
Accept: application/json

Respuesta conocida:

{
  "sku": "BAG-URBAN",
  "reservableQuantity": 3
}

Los dos equipos fijan estas reglas:

  • El SKU contiene entre uno y 64 caracteres alfanuméricos, guiones o guiones bajos.
  • reservableQuantity es un entero seguro no negativo.
  • Un SKU conocido sin unidades devuelve 200 con cantidad cero.
  • Un SKU desconocido devuelve 404.
  • Un parámetro inválido devuelve 400.
  • Un error del proveedor se comunica como error técnico, no como cantidad cero.
  • La respuesta es una observación, no una promesa de reserva.

Para un contrato real también hay que concretar autorización, límites de uso, caché y tiempos de respuesta. No los resuelve el DTO. En este ejemplo no mostramos credenciales ni configuración de infraestructura.

Un consumidor que no importa el modelo del proveedor

Sales define un puerto según su necesidad. La representación de red queda localizada en su adaptador, sin importar entidades o repositorios internos de Inventory.

export interface AvailabilitySnapshot {
  readonly sku: string;
  readonly reservableQuantity: number;
}

export interface AvailabilityReader {
  read(sku: string): Promise<AvailabilitySnapshot>;
}

Estas interfaces ayudan a escribir y revisar código, pero no controlan el proceso remoto. Un JSON puede contener una cadena donde TypeScript espera un número.

La siguiente función comprueba datos desconocidos y construye la representación local. Los fragmentos de esta sección se pueden organizar en un único archivo inventory-availability.ts; no son una implementación del servidor Inventory.

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null &&
    !Array.isArray(value);
}

export function parseAvailability(
  raw: unknown,
  requestedSku: string,
): AvailabilitySnapshot {
  if (!isRecord(raw) || raw.sku !== requestedSku) {
    throw new Error("La respuesta no corresponde al SKU solicitado");
  }

  const quantity = raw.reservableQuantity;
  if (
    typeof quantity !== "number" ||
    !Number.isSafeInteger(quantity) ||
    quantity < 0
  ) {
    throw new Error("Disponibilidad inválida");
  }

  return {
    sku: requestedSku,
    reservableQuantity: quantity,
  };
}

El consumidor acepta propiedades adicionales porque el acuerdo permite ampliaciones que no cambien el significado de los campos conocidos. No conserva ni utiliza esas propiedades.

El adaptador HTTP

Este fragmento complementa los anteriores. Presupone un entorno Node.js con fetch, URL y AbortSignal.timeout disponibles y configuración de TypeScript adecuada para esas APIs.

type Fetcher = typeof fetch;

export class UnknownSkuError extends Error {}

export class InventoryAvailabilityHttp implements AvailabilityReader {
  constructor(
    private readonly origin: string,
    private readonly fetcher: Fetcher = fetch,
  ) {}

  async read(sku: string): Promise<AvailabilitySnapshot> {
    if (!/^[A-Za-z0-9_-]{1,64}$/.test(sku)) {
      throw new Error("SKU inválido");
    }

    const url = new URL("/api/v1/availability", this.origin);
    url.searchParams.set("sku", sku);

    const response = await this.fetcher(url, {
      headers: { accept: "application/json" },
      signal: AbortSignal.timeout(3000),
      redirect: "error",
      cache: "no-store",
    });

    if (response.status === 404) {
      throw new UnknownSkuError("Inventory no reconoce el SKU");
    }

    if (!response.ok) {
      throw new Error("No se pudo consultar Inventory");
    }

    const raw: unknown = await response.json();
    return parseAvailability(raw, sku);
  }
}

La URL base viene de configuración controlada, no de datos enviados por el usuario. El tiempo máximo es una decisión didáctica; hay que negociarlo según el uso real.

No seguimos redirecciones ni convertimos fallos de red, JSON inválido o agotamiento del tiempo en una aceptación. Una versión de producción debe concretar autenticación y, si lo requiere su exposición, límites de tamaño de respuesta y observabilidad.

El adaptador actúa como borde de traducción de Sales. Su propósito aquí es demostrar la dependencia explícita; no necesitamos desarrollar un framework de integración.

La decisión local de Sales

export async function canAttemptReservation(
  sku: string,
  requestedQuantity: number,
  availability: AvailabilityReader,
): Promise<boolean> {
  if (!Number.isSafeInteger(requestedQuantity) || requestedQuantity <= 0) {
    throw new Error("La cantidad solicitada debe ser positiva");
  }

  const snapshot = await availability.read(sku);
  return snapshot.reservableQuantity >= requestedQuantity;
}

Si la consulta devuelve tres unidades y Sales necesita dos, el resultado es true: puede intentar la reserva. No significa que el pedido esté confirmado.

No mostramos persistencia porque esta operación solo consulta. Una futura operación de reserva debe tener su propio contrato y confirmar el resultado efectivo. Entre la consulta y esa operación otro comprador puede haber consumido las unidades.

Aunque ambos contextos estén dentro de un monolito modular, la llamada HTTP no crea una transacción compartida. No permitas que el nombre de la función prometa una garantía que el contrato no ofrece.

Pruebas que hacen visible el acuerdo

Necesitamos distinguir tres comprobaciones:

  1. Sales interpreta correctamente respuestas y errores.
  2. Inventory produce respuestas acordadas ante estados conocidos.
  3. El intercambio real conserva ruta, parámetros, estados HTTP y cuerpo esperado.

Un mock escrito por Sales solo demuestra su propia expectativa. Para comprobar compatibilidad hay que verificarla frente a Inventory. Herramientas de pruebas de contrato como Pact organizan esa relación entre interacciones del consumidor y verificación del proveedor; no es obligatorio incorporar una para comprender el mecanismo. Pact: cómo funciona.

Pruebas unitarias del consumidor

Estos ejemplos usan APIs integradas de Node.js y presuponen compilación a JavaScript ESM. Complementan el archivo anterior; no se han ejecutado como parte de este artículo.

import test from "node:test";
import assert from "node:assert/strict";
import {
  parseAvailability,
  canAttemptReservation,
} from "./inventory-availability.js";

test("acepta un campo adicional sin cambiar la interpretación", () => {
  assert.deepEqual(parseAvailability({
    sku: "BAG-URBAN",
    reservableQuantity: 3,
    observedAt: "2026-01-01T10:00:00Z",
  }, "BAG-URBAN"), {
    sku: "BAG-URBAN",
    reservableQuantity: 3,
  });
});

test("rechaza una cantidad enviada como texto", () => {
  assert.throws(() => parseAvailability({
    sku: "BAG-URBAN",
    reservableQuantity: "3",
  }, "BAG-URBAN"));
});

test("rechaza la respuesta de otro SKU", () => {
  assert.throws(() => parseAvailability({
    sku: "OTHER",
    reservableQuantity: 3,
  }, "BAG-URBAN"));
});

test("Sales permite intentar dos unidades cuando observa tres", async () => {
  const result = await canAttemptReservation("BAG-URBAN", 2, {
    async read(sku) {
      return { sku, reservableQuantity: 3 };
    },
  });
  assert.equal(result, true);
});

Estas pruebas no arrancan Inventory. Protegen la interpretación de Sales, no demuestran que exista el endpoint ni que su proveedor respete el contrato.

Una verificación contra el proveedor real de pruebas

Los equipos pueden acordar un estado de prueba: BAG-URBAN existe y tiene tres unidades reservables. La preparación pertenece a Inventory y debe hacerse en un entorno aislado de pruebas.

El siguiente comprobador recibe la URL de ese entorno y consulta su endpoint real. No debe ejecutarse contra producción ni asumir datos de una tienda real.

import assert from "node:assert/strict";
import { parseAvailability } from "./inventory-availability.js";

export async function verifyKnownAvailabilityContract(
  providerOrigin: string,
): Promise<void> {
  const url = new URL("/api/v1/availability", providerOrigin);
  url.searchParams.set("sku", "BAG-URBAN");

  const response = await fetch(url, {
    headers: { accept: "application/json" },
    signal: AbortSignal.timeout(3000),
    redirect: "error",
    cache: "no-store",
  });

  assert.equal(response.status, 200);
  const raw: unknown = await response.json();

  assert.deepEqual(parseAvailability(raw, "BAG-URBAN"), {
    sku: "BAG-URBAN",
    reservableQuantity: 3,
  });
}

Es una verificación mínima de una interacción, no una suite completa ni una configuración de Pact. Faltan el arranque del proveedor, la preparación del estado, las credenciales si corresponden y los escenarios de cero unidades, 404 y 400.

Inventory debe incluir esos escenarios en su verificación antes de entregar una versión. Sales, por su parte, necesita pruebas de su adaptador que comprueben la petición enviada y el tratamiento de errores. Mantener ambas partes evita que un doble y una implementación evolucionen con supuestos diferentes.

Las pruebas del contrato tampoco sustituyen pruebas internas de Inventory sobre el cálculo de existencias. Una respuesta con formato correcto podría contener una cantidad mal calculada.

Evolucionar sin romper a Sales

Supongamos que Inventory quiere añadir un momento de observación:

{
  "sku": "BAG-URBAN",
  "reservableQuantity": 3,
  "observedAt": "2026-01-01T10:00:00Z"
}

Para el consumidor mostrado, agregar ese campo es compatible: permite propiedades nuevas y mantiene los campos conocidos. Eso no demuestra compatibilidad con todos los consumidores; alguno podría rechazar propiedades adicionales.

La propuesta de cambio debe verificar:

  • Si los consumidores conocidos toleran el campo.
  • Si se mantienen significado, tipo y obligatoriedad de los anteriores.
  • Si el coste adicional de calcularlo altera las expectativas de respuesta.
  • Si se necesita actualizar documentación y ejemplos.

En cambio, estos cambios requieren otra negociación:

  • Renombrar reservableQuantity y retirar el campo antiguo.
  • Devolver la cantidad como cadena.
  • Convertir 404 en 200 con un cuerpo distinto sin actualizar el acuerdo.
  • Incluir unidades retenidas dentro de una cantidad antes definida como reservable.

El último es especialmente peligroso: puede pasar una validación estructural y seguir rompiendo la decisión comercial.

Un plan de transición concreto

Para sustituir un campo, una opción es añadir primero la representación nueva, mantener temporalmente la anterior y comprobar que todos los consumidores migran. Solo después se retira la antigua según un acuerdo verificable.

Si cambia la semántica de forma importante, puede ser más claro ofrecer una versión o capacidad separada. Un sufijo v2 no resuelve por sí solo qué significa cada respuesta o cuánto tiempo convivirán.

En la planificación deben constar responsables, criterios de aceptación y condición de retirada. «Avisamos por chat» no demuestra que Sales pueda adoptar el cambio a tiempo.

La relación se mantiene durante el trabajo

Una ficha breve ayuda a administrar la colaboración:

Proveedor: equipo de Inventory.
Consumidor: equipo de Sales.
Necesidad: observar unidades reservables para orientar un intento de reserva.
Compromiso funcional: campos y errores descritos mediante ejemplos acordados.
Evolución: revisar cambios que alteren esas expectativas y verificar consumidores afectados.
Entrega: fecha negociada y evidencia de los escenarios de aceptación.
Límite: no se promete que una consulta asegure unidades.

No inventes fechas o niveles de servicio para rellenar el documento. Si todavía no están acordados, regístralos como pendientes.

El proveedor debe poder decir que una necesidad no cabe en la entrega actual y negociar una alternativa. Eso es más útil que aceptar solicitudes que nunca entran en planificación.

La colaboración tampoco depende de ser dos departamentos enormes. En un equipo pequeño, pueden existir responsables claros de ambos contextos y una revisión breve de contratos.

Diferencias con relaciones cercanas

Conformist: el consumidor adopta el modelo ofrecido sin la misma capacidad de influir en su evolución. Si Sales solo puede esperar lo que Inventory decida, no describas la relación como Customer/Supplier por cortesía.

Partnership: los equipos coordinan una relación donde el resultado depende del éxito conjunto. Customer/Supplier mantiene una asimetría: el consumidor depende del proveedor, cuyas prioridades deben incorporar compromisos negociados.

Shared Kernel: comparte deliberadamente una parte del modelo con gobierno conjunto. Customer/Supplier puede trabajar con contratos explícitos y modelos independientes, sin una biblioteca común de entidades.

Una ACL puede proteger a Sales dentro de una relación Customer/Supplier. No reemplaza la negociación; traduce el contrato acordado. Del mismo modo, eventos de integración pueden transportar información, pero cambiar HTTP por eventos no crea colaboración entre equipos automáticamente.

Dos situaciones donde la decisión cambia

Educación: inscripciones necesita una confirmación de pago

Inscripciones depende de Pagos para habilitar una matrícula. Ambos equipos acuerdan qué estado permite esa acción y qué información necesitan para identificar la operación.

Customer/Supplier tiene sentido si Pagos incorpora esa necesidad en su planificación y ambos verifican ejemplos de aceptación. La palabra «pago» no basta: un intento iniciado no equivale a fondos confirmados.

Este ejemplo enseña a negociar significado antes de automatizar una decisión con consecuencias para el usuario.

Logística: un transportista que no acepta cambios

Una tienda consume una API pública de seguimiento. El transportista no negocia su contrato con ese cliente pequeño.

Puede existir una relación comercial, pero eso no demuestra Customer/Supplier Development entre los equipos. La tienda tendrá que decidir si adopta el modelo ofrecido o mantiene una traducción local, documentando las limitaciones.

Para una página que solo muestra un enlace al seguimiento, una integración profunda quizá no compense. La opción sencilla puede satisfacer la necesidad sin fingir un acuerdo que no existe.

Contexto útil para una IA

Estos prompts se pueden usar con Cursor, Claude o Codex después de adjuntar los acuerdos y ejemplos reales:

Para analizar la relación:

Inventory ofrece disponibilidad a Sales. Con estas actas y contratos, distingue compromisos confirmados de suposiciones. Explica si hay Customer/Supplier real y qué necesidades faltan en planificación. No deduzcas Upstream por quién inicia HTTP.

Para implementar el consumidor:

Implementa un adaptador HTTP propiedad de Sales para este contrato. Trata el cuerpo como unknown y valida SKU y cantidad. Distingue SKU desconocido, cero unidades y fallo técnico. No importes entidades ni repositorios de Inventory. La consulta no garantiza una reserva.

Para revisar un cambio:

Contrasta esta versión del proveedor con las expectativas de Sales. Identifica cambios estructurales y semánticos, consumidores afectados y pruebas que faltan. Propón una transición compatible con criterios de retirada. No inventes que los equipos aprobaron el plan.

La persona responsable debe verificar que el contrato existe, que su significado coincide con el negocio y que los compromisos están aceptados. La IA puede analizar dependencias; no puede comprometer la capacidad de otro equipo.

Ejercicios de aplicación

1. El campo que cambia de significado

Inventory mantiene reservableQuantity, pero ahora incluye unidades retenidas. Los tests de JSON siguen pasando. ¿Es compatible?

2. El mock siempre verde

Sales prueba todas sus decisiones con un doble que devuelve tres unidades. Inventory cambia el nombre del campo. ¿Qué comprobación falta?

3. La petición ignorada

Sales pide reservas con caducidad. Inventory responde que quizá las implemente algún día, sin plan ni compromiso. ¿Qué debería documentarse?

Soluciones razonadas

1. No para el acuerdo del ejemplo. Sales interpreta esa cantidad como reservable. Hay que conservar la semántica anterior o negociar otra representación y adaptar decisiones y pruebas. Mantener un número no garantiza compatibilidad.

2. Falta verificar la interacción contra el proveedor y probar el adaptador del consumidor. El doble solo reproduce una expectativa local. Los escenarios acordados deben comprobarse sobre una versión real de Inventory en un entorno preparado.

3. Que la capacidad no está comprometida y Sales no puede depender de ella como si existiera. Deben negociar alcance y fecha o elegir una alternativa viable. Etiquetar la relación no reemplaza el acuerdo de entrega.

Reconocer un acuerdo que funciona

Customer/Supplier aporta valor cuando una dependencia técnica necesita influencia y compromisos concretos entre responsables.

Comprueba si una necesidad del consumidor puede llegar a la planificación del proveedor, si ambos entienden el significado de las respuestas y si existe evidencia de compatibilidad antes de entregar cambios.

Cuando esas tres cosas son visibles, el contrato deja de ser una interfaz escrita por un solo equipo y se convierte en una colaboración que ambos pueden sostener.

Cargando tus opciones de aprendizaje…

Cargando conversación…

Sigue aprendiendo