En una tienda online, marketing quiere saber si un cliente acepta recibir promociones. Facturación necesita su nombre fiscal y su NIF. Envíos busca una dirección y un teléfono de contacto. Como los tres hablan de «el cliente», alguien crea una sola clase para todos:
// Counterexample: one Customer for every department
class Customer {
private String email;
private boolean acceptsPromotions;
private String legalName;
private String taxId;
private String shippingAddress;
private String phone;
private String status; // "ACTIVE" means something different to each team
}
Al principio parece ordenado. Luego facturación pide que taxId sea obligatorio, y resulta que muchos clientes que solo compran como particulares no lo tienen. Marketing añade un estado "UNSUBSCRIBED", y envíos empieza a no preparar pedidos porque interpreta que el cliente ya no está activo. Cada cambio razonable para un equipo rompe algo para otro.
El problema no es la clase en sí. Es que la palabra «cliente» tiene significados distintos según quién la use, y la clase intenta representarlos todos a la vez. Un contexto delimitado, bounded context en inglés, es la frontera dentro de la cual un modelo y su vocabulario tienen un significado único y coherente.
La persona es la misma; el papel cambia
Pensemos en Lucía. Compra un regalo, pide factura a nombre de su empresa y lo manda a casa de su hermano. Para marketing es alguien a quien se puede escribir. Para facturación, el destinatario fiscal es su empresa. Para envíos, la persona que recibe el paquete ni siquiera es ella.
Son tres papeles distintos de una misma persona, y cada uno tiene sus datos y sus reglas. En lugar de un Customer universal, cada contexto define el modelo que necesita:
// marketing context
class Subscriber {
private final String customerId;
private final String email;
private boolean acceptsPromotions;
public Subscriber(String customerId, String email, boolean acceptsPromotions) {
this.customerId = customerId;
this.email = email;
this.acceptsPromotions = acceptsPromotions;
}
public boolean canReceivePromotions() {
return acceptsPromotions;
}
public void unsubscribe() {
acceptsPromotions = false;
}
}
// billing context
class InvoiceRecipient {
private final String customerId;
private final String legalName;
private final String taxId;
public InvoiceRecipient(String customerId, String legalName, String taxId) {
if (taxId == null || taxId.trim().isEmpty()) {
throw new IllegalArgumentException("Tax ID is required for an invoice.");
}
this.customerId = customerId;
this.legalName = legalName;
this.taxId = taxId;
}
public String getLegalName() {
return legalName;
}
public String getTaxId() {
return taxId;
}
}
Subscriber sabe si se pueden enviar promociones y permite darse de baja. InvoiceRecipient exige un NIF porque, en facturación, sin él no hay factura posible. Esa regla vive donde tiene sentido y ya no afecta a quien solo compra como particular. Darse de baja en marketing tampoco puede detener un envío, porque envíos ni siquiera conoce ese dato.
Los dos modelos comparten customerId. Un identificador común permite saber que hablamos de la misma persona sin obligar a reunir sus datos en un objeto que todos modifican. Envíos tendría su propio DeliveryContact, con un nombre, una dirección y un teléfono que, como en el regalo de Lucía, pueden no ser los de quien compra.
Martin Fowler explica en su artículo sobre Bounded Context que estas diferencias de vocabulario son una de las señales más claras para trazar fronteras. Si dos equipos usan la misma palabra y no se ponen de acuerdo en su significado, probablemente hablan desde contextos distintos.
Un mapa de contextos y sus conversaciones
Separar modelos no significa aislarlos. Los contextos colaboran, y esa colaboración también se diseña. En la tienda, el contexto de compras pide un cobro al de pagos; pagos comunica el resultado, y envíos recibe la autorización para preparar el paquete cuando el pedido está pagado.

En la parte superior del diagrama, cada contexto tiene su propio modelo de la misma persona, conectados solo por el identificador. En la inferior, las flechas muestran por dónde viaja la información de un pago: del proveedor externo a nuestra traducción y de ahí a los contextos que la usan.
Cada relación entre contextos necesita respuesta a unas preguntas: quién define el contrato, quién se adapta a él, cómo se comunican los cambios y qué ocurre si el otro lado no responde. Son las preguntas de dependencias y contratos, ahora entre partes enteras del sistema.
Traducir el lenguaje de un sistema externo
El proveedor de pagos tiene su propio vocabulario: devuelve textos como "CAPTURED", "PARTIAL_CAPTURE" o "REJECTED". Si dejamos que esos textos se extiendan por la tienda, nuestro modelo acabará hablando el idioma del proveedor, y un cambio suyo obligará a revisar medio programa.
Una capa anticorrupción es una pieza que traduce el modelo de un sistema externo al nuestro y evita que sus conceptos se cuelen dentro. Primero definimos nuestro vocabulario:
enum PaymentState {
CAPTURED,
PARTIALLY_CAPTURED,
DECLINED,
UNKNOWN
}
class PaymentStatus {
private final PaymentState state;
private final int capturedInCents;
public PaymentStatus(PaymentState state, int capturedInCents) {
this.state = state;
this.capturedInCents = capturedInCents;
}
public PaymentState getState() {
return state;
}
public int getCapturedInCents() {
return capturedInCents;
}
}
CAPTURED significa que se ha cobrado el importe; PARTIALLY_CAPTURED, que solo se ha cobrado una parte. UNKNOWN cubre cualquier respuesta que no sepamos interpretar. Después, la traducción:
class ProviderPaymentTranslator {
public PaymentStatus translate(String providerStatus, int capturedInCents) {
if (capturedInCents < 0) {
throw new IllegalArgumentException("Captured amount cannot be negative.");
}
return new PaymentStatus(toState(providerStatus), capturedInCents);
}
private PaymentState toState(String providerStatus) {
if ("CAPTURED".equals(providerStatus)) {
return PaymentState.CAPTURED;
}
if ("PARTIAL_CAPTURE".equals(providerStatus)) {
return PaymentState.PARTIALLY_CAPTURED;
}
if ("REJECTED".equals(providerStatus)) {
return PaymentState.DECLINED;
}
return PaymentState.UNKNOWN;
}
}
Escribir "CAPTURED".equals(providerStatus) en lugar de providerStatus.equals("CAPTURED") evita un error si el proveedor no envía estado: con un valor nulo, el método simplemente llega al final y devuelve UNKNOWN. Los códigos del proveedor son inventados para el ejemplo; una integración real comprobaría también la moneda y el formato completo de la respuesta.
Esta traducción es un adaptador, en el mismo sentido que en la arquitectura hexagonal: el detalle externo se queda en el borde y el interior trabaja con conceptos propios.
Por qué no basta con «pagado»
Ahora, el contexto que decide si se puede preparar un envío usa nuestro vocabulario:
class ShippingAuthorization {
public boolean canPrepare(PaymentStatus payment, int orderTotalInCents) {
return payment.getState() == PaymentState.CAPTURED
&& payment.getCapturedInCents() == orderTotalInCents;
}
}
Sigamos cuatro respuestas del proveedor para un pedido de 5000 céntimos:
ProviderPaymentTranslator translator = new ProviderPaymentTranslator();
ShippingAuthorization shipping = new ShippingAuthorization();
PaymentStatus full = translator.translate("CAPTURED", 5000);
PaymentStatus partial = translator.translate("PARTIAL_CAPTURE", 2000);
PaymentStatus rejected = translator.translate("REJECTED", 0);
PaymentStatus odd = translator.translate("NEW", 0);
shipping.canPrepare(full, 5000); // true
shipping.canPrepare(partial, 5000); // false: only 2000 were captured
partial.getState(); // PARTIALLY_CAPTURED
rejected.getState(); // DECLINED
odd.getState(); // UNKNOWN
El cobro completo autoriza el envío. El parcial no, y la razón queda a la vista: solo se han cobrado 2000 de 5000 céntimos. Si la traducción hubiera reducido "CAPTURED" y "PARTIAL_CAPTURE" a un único PAID, envíos habría preparado un paquete que no está pagado del todo. La capa anticorrupción no consiste en simplificar a toda costa, sino en conservar las distinciones que cambian nuestras decisiones.
El estado "NEW" no aparece en nuestra lista y se traduce como UNKNOWN. Ese valor no significa ni éxito ni rechazo: indica que hay que consultar o investigar. Convertirlo en un rechazo podría llevar a anular un pedido que sí se cobró.
Un contexto no es un microservicio
Un contexto delimitado es una frontera de significado, no de despliegue. Marketing, facturación y envíos pueden ser tres módulos de un monolito modular, cada uno con su modelo y su contrato público. Si algún día conviene separar alguno en un microservicio, la frontera ya estará definida. Al revés no funciona: repartir un Customer confuso en tres servicios no aclara qué significa «cliente».
Hay más de una forma de relacionarse con otro modelo. A veces compensa adoptar directamente el del proveedor, si es pequeño, estable y encaja con nuestras necesidades: nos ahorramos la traducción y aceptamos depender de sus cambios. Otras veces dos contextos comparten deliberadamente una parte pequeña del modelo, como un identificador o un tipo de dinero, sabiendo que cada cambio en esa parte exige ponerse de acuerdo. El riesgo es que esa parte compartida crezca hasta contener las reglas de media tienda y volvamos al Customer del principio.
Subdominios y contextos
En la introducción a DDD vimos los subdominios, las partes del problema del negocio. Los contextos son otra cosa: fronteras del modelo de software. A menudo coinciden, pero no siempre. Un sistema antiguo puede mezclar varios subdominios en un mismo modelo, y un subdominio complejo puede necesitar más de un contexto.
No hay una fórmula que dibuje el mapa correcto. Algunas señales ayudan: palabras que cambian de significado, equipos distintos que toman las decisiones, reglas que cambian a ritmos diferentes. Las fronteras son hipótesis que se revisan cuando un cambio o un incidente muestra que estaban mal colocadas.
Tampoco cada papel distinto pide un contexto propio. Dentro del mismo modelo de compras pueden convivir quien hace el pedido y quien lo recibe, con dos campos y una regla clara, sin que nadie dude de qué significa cada uno. El papel es una pista; la señal de verdad es el conflicto de significado. Y tener carpetas o módulos separados no demuestra que haya varios contextos: si comparten vocabulario y reglas sin contradicciones, son un solo contexto organizado en varias partes.
Para empezar, elige una palabra que use toda la empresa, como «pagado» o «cliente», y pide a cada equipo un ejemplo de lo que significa para él. Si las respuestas llevan a decisiones distintas, ya has encontrado una frontera. Y si una traducción entre contextos hace que dos situaciones diferentes acaben con el mismo nombre, esa traducción está escondiendo algo que alguien necesitará saber.