DDD en la práctica: Bounded Contexts y Context Mapping
Guía práctica de bounded contexts y context mapping: los patrones estratégicos de DDD que evitan la corrupción del modelo de dominio.

Por qué los bounded contexts importan más que los aggregates
La mayoría de las introducciones a Domain-Driven Design se centran en patrones tácticos: entidades, objetos de valor, aggregates, repositorios. Son útiles, pero son detalles de implementación. Los patrones estratégicos—bounded contexts, context maps y ubiquitous language—son los que determinan si un sistema escala con la complejidad organizacional o colapsa bajo ella.
Un bounded context es un límite dentro del cual un modelo de dominio particular se define y aplica. El mismo concepto del mundo real—"cliente", "pedido", "producto"—significa cosas distintas en distintas partes de tu sistema. Intentar imponer un único modelo unificado en todos los contextos crea un embrollo frágil y lleno de compromisos que no satisface a nadie.
Cómo identificar los límites de un bounded context
Los bounded contexts se alinean con las áreas donde el lenguaje cambia de significado. Cuando el equipo de ventas dice "cliente" se refiere a algo distinto de lo que significa para el equipo de facturación, que a su vez difiere de lo que entiende el equipo de soporte.
// ❌ Single "Customer" model trying to serve every context
interface UnifiedCustomer {
id: string;
name: string;
email: string;
// Sales context fields
leadScore: number;
salesRepId: string;
pipelineStage: string;
// Billing context fields
paymentMethod: string;
billingAddress: string;
taxId: string;
creditLimit: number;
// Support context fields
supportTier: string;
openTickets: number;
satisfactionScore: number;
preferredContactMethod: string;
}
// ✅ Each context owns its own model
// --- Sales Context ---
interface Lead {
id: string;
contactName: string;
contactEmail: string;
score: number;
assignedRepId: string;
stage: "prospect" | "qualified" | "proposal" | "negotiation" | "closed";
}
// --- Billing Context ---
interface BillingAccount {
accountId: string;
accountHolder: string;
paymentMethod: PaymentMethod;
billingAddress: Address;
taxId: string | null;
creditLimit: number;
}
// --- Support Context ---
interface SupportContact {
contactId: string;
displayName: string;
tier: "basic" | "premium" | "enterprise";
preferredChannel: "email" | "phone" | "chat";
}Cada modelo contiene solo lo que su contexto necesita. Al contexto de ventas no le importan las direcciones de facturación. Al contexto de soporte no le hacen falta los lead scores. Esto no es redundancia, es modelado adecuado.
Patrones de context mapping
Los bounded contexts no existen aislados. Necesitan intercambiar información. El context mapping define las relaciones y patrones de integración entre contextos.
enum ContextRelationship {
PARTNERSHIP = "partnership",
SHARED_KERNEL = "shared-kernel",
CUSTOMER_SUPPLIER = "customer-supplier",
CONFORMIST = "conformist",
ANTICORRUPTION_LAYER = "anticorruption-layer",
OPEN_HOST_SERVICE = "open-host-service",
PUBLISHED_LANGUAGE = "published-language",
SEPARATE_WAYS = "separate-ways",
}
interface ContextMapEntry {
upstream: string;
downstream: string;
relationship: ContextRelationship;
integrationPattern: string;
dataFlow: string;
}
const contextMap: ContextMapEntry[] = [
{
upstream: "Sales",
downstream: "Billing",
relationship: ContextRelationship.CUSTOMER_SUPPLIER,
integrationPattern: "Domain events via message queue",
dataFlow: "Sales publishes LeadConverted, Billing creates BillingAccount",
},
{
upstream: "Legacy ERP",
downstream: "Order Management",
relationship: ContextRelationship.ANTICORRUPTION_LAYER,
integrationPattern: "ACL translates ERP XML into domain events",
dataFlow: "ERP exports orders, ACL transforms into Order aggregate",
},
{
upstream: "Order Management",
downstream: "Shipping",
relationship: ContextRelationship.OPEN_HOST_SERVICE,
integrationPattern: "REST API with published schema",
dataFlow: "Shipping queries order details via versioned API",
},
];El context map no es un diagrama técnico, es un documento político. Describe quién depende de quién, quién tiene el poder de cambiar interfaces y dónde se necesitan capas de traducción.
Cómo implementar una anti-corruption layer
La anti-corruption layer (ACL) es el patrón de integración más importante para proteger tu modelo de dominio de influencias externas. Traduce entre el lenguaje de un contexto upstream y el tuyo propio.
// External legacy system model (upstream — we don't control this)
interface LegacyOrderRecord {
ORD_NUM: string;
CUST_ID: string;
ORD_DT: string; // Format: YYYYMMDD
ITEM_LST: string; // Pipe-separated: "SKU1|QTY1|PRC1||SKU2|QTY2|PRC2"
TOT_AMT: number; // In cents
STAT_CD: number; // 1=pending, 2=confirmed, 3=shipped, 4=delivered, 9=cancelled
}
// Our domain model (downstream — this is our bounded context)
interface Order {
orderId: string;
customerId: string;
placedAt: Date;
items: OrderItem[];
total: Money;
status: OrderStatus;
}
interface OrderItem {
sku: string;
quantity: number;
unitPrice: Money;
}
interface Money {
amount: number;
currency: string;
}
type OrderStatus =
| "pending"
| "confirmed"
| "shipped"
| "delivered"
| "cancelled";
// Anti-Corruption Layer
class OrderAntiCorruptionLayer {
private static readonly STATUS_MAP: Record<number, OrderStatus> = {
1: "pending",
2: "confirmed",
3: "shipped",
4: "delivered",
9: "cancelled",
};
translate(legacy: LegacyOrderRecord): Order {
return {
orderId: legacy.ORD_NUM,
customerId: legacy.CUST_ID,
placedAt: this.parseDate(legacy.ORD_DT),
items: this.parseItems(legacy.ITEM_LST),
total: { amount: legacy.TOT_AMT / 100, currency: "USD" },
status: this.mapStatus(legacy.STAT_CD),
};
}
private parseDate(dateStr: string): Date {
const year = parseInt(dateStr.slice(0, 4), 10);
const month = parseInt(dateStr.slice(4, 6), 10) - 1;
const day = parseInt(dateStr.slice(6, 8), 10);
return new Date(year, month, day);
}
private parseItems(itemList: string): OrderItem[] {
const parts = itemList.split("||");
return parts.map((part) => {
const [sku, qty, price] = part.split("|");
return {
sku,
quantity: parseInt(qty, 10),
unitPrice: { amount: parseFloat(price) / 100, currency: "USD" },
};
});
}
private mapStatus(code: number): OrderStatus {
const status = OrderAntiCorruptionLayer.STATUS_MAP[code];
if (!status) {
throw new Error(`Unknown legacy status code: ${code}`);
}
return status;
}
}La ACL protege al modelo de dominio de las convenciones de nombres, formatos de datos y decisiones estructurales del sistema legado. Si el sistema upstream cambia, solo hay que actualizar la ACL; el modelo de dominio permanece puro.
Eventos de dominio para la integración entre contextos
Los domain events son el mecanismo principal para que los bounded contexts se comuniquen sin acoplamiento. Cada contexto publica eventos sobre cambios de estado significativos; otros contextos se suscriben a los que les importan.
interface DomainEvent {
eventId: string;
eventType: string;
occurredAt: Date;
aggregateId: string;
payload: Record<string, unknown>;
}
// Sales context publishes when a lead converts
interface LeadConvertedEvent extends DomainEvent {
eventType: "sales.lead.converted";
payload: {
leadId: string;
contactName: string;
contactEmail: string;
contractValue: number;
salesRepId: string;
};
}
// Billing context subscribes and creates a billing account
class BillingEventHandler {
constructor(private readonly accountRepo: AccountRepository) {}
async handleLeadConverted(event: LeadConvertedEvent): Promise<void> {
const account: BillingAccount = {
accountId: crypto.randomUUID(),
accountHolder: event.payload.contactName,
paymentMethod: { type: "pending-setup" },
billingAddress: { type: "pending-collection" },
taxId: null,
creditLimit: this.calculateInitialCredit(event.payload.contractValue),
};
await this.accountRepo.save(account);
}
private calculateInitialCredit(contractValue: number): number {
return Math.min(contractValue * 0.1, 10000);
}
}
// Support context subscribes and creates a support record
class SupportEventHandler {
constructor(private readonly contactRepo: ContactRepository) {}
async handleLeadConverted(event: LeadConvertedEvent): Promise<void> {
const contact: SupportContact = {
contactId: crypto.randomUUID(),
displayName: event.payload.contactName,
tier: "basic",
preferredChannel: "email",
};
await this.contactRepo.save(contact);
}
}Cada contexto downstream traduce el evento a su propio lenguaje y crea sus propios objetos de dominio. El contexto de ventas no necesita saber que existen facturación y soporte: simplemente anuncia lo que ocurrió.
Shared kernel: cuando los contextos necesitan un terreno común
A veces dos contextos están tan relacionados que mantener modelos completamente separados crea más problemas que compartirlos. El patrón shared kernel permite que dos contextos compartan un subconjunto pequeño y definido explícitamente del modelo de dominio.
// Shared kernel between Order and Shipping contexts
// This module is co-owned by both teams and changes require agreement
// shared-kernel/money.ts
export class Money {
constructor(
public readonly amount: number,
public readonly currency: string
) {
if (amount < 0) throw new Error("Money amount cannot be negative");
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("Cannot add different currencies");
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
// shared-kernel/address.ts
export interface Address {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
}
// shared-kernel/product-reference.ts
export interface ProductReference {
sku: string;
name: string;
weight: { value: number; unit: "kg" | "lb" };
}El shared kernel debe ser pequeño, estable y mantenido conjuntamente. Si un equipo quiere cambiarlo unilateralmente, el kernel es demasiado grande o la relación en realidad es customer-supplier, no shared kernel.
Conclusiones clave
Los bounded contexts son el patrón de DDD con más impacto porque evitan la corrupción del modelo de dominio que vuelve ingobernables los sistemas grandes. Identifica los límites donde el lenguaje cambia de significado, no donde crees que deberían estar los límites de los microservicios. Usa context mapping para dejar explícitas las relaciones políticas entre contextos.
Protege tu modelo de dominio de los sistemas externos con anti-corruption layers. Comunícate entre contextos usando domain events en lugar de llamadas directas a API: los eventos preservan la autonomía y permiten el despliegue independiente. Cuando dos contextos realmente necesitan conceptos compartidos, usa un shared kernel, pero mantenlo mínimo y gestionado conjuntamente.
El objetivo no es eliminar todo el acoplamiento, sino hacerlo intencional, explícito y manejable. Un context map bien dibujado vale más que cualquier cantidad de patrones tácticos de DDD aplicados dentro de un único contexto desbordado.


