En sistemas reales, los Bounded Contexts rara vez existen de forma aislada. Cuando dos contextos necesitan colaborar, sus modelos de dominio inevitablemente difieren: cada uno fue diseñado para resolver un problema distinto, con su propio Ubiquitous Language, sus propias reglas de negocio y sus propias abstracciones. Si permitimos que el modelo de un contexto externo se filtre hacia nuestro dominio, terminamos con un acoplamiento semántico que erosiona la integridad conceptual del sistema.
El Anti-Corruption Layer (ACL) es un patrón estratégico de Domain-Driven Design que actúa como una barrera de traducción entre Bounded Contexts. Su propósito es proteger la integridad del modelo de dominio propio frente a conceptos, estructuras y lenguaje de un modelo externo o legacy.
Cuando un Bounded Context downstream (consumidor) depende de un contexto upstream (proveedor), existen varios riesgos sin una capa de protección:
- Contaminación del Ubiquitous Language: el equipo comienza a usar términos del contexto externo que no tienen sentido en su propio dominio.
- Acoplamiento estructural: cambios en el modelo upstream fuerzan cambios en cascada en el downstream.
- Pérdida de invariantes de dominio: las reglas de negocio del contexto propio se diluyen al adaptarse a las estructuras ajenas.
- Deuda técnica compuesta: cada integración directa multiplica los puntos de fricción ante cualquier evolución del sistema.
El ACL establece un límite explícito de traducción que absorbe estas diferencias, permitiendo que cada contexto evolucione de forma independiente.
El ACL se compone típicamente de tres elementos colaborativos:
┌─────────────────────────────────────────────────────────────┐
│ Bounded Context Downstream │
│ (Authorizations) │
│ │
│ ┌──────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ Domain │◄───│ Application │◄───│ Anti-Corruption │ │
│ │ Model │ │ Port │ │ Layer │ │
│ │ │ │ (interface) │ │ │ │
│ │ Coverage │ │ VerifyCoverage│ │ ┌──────────────┐ │ │
│ │ Benefits │ │ │ │ │ Translator │ │ │
│ │ Document │ │ │ │ │ │ │ │
│ │ │ │ │ │ └──────────────┘ │ │
│ └──────────┘ └───────────────┘ └────────┬─────────┘ │
│ │ │
└────────────────────────────────────────────────┼─────────────┘
│
▼
┌──────────────────────────┐
│ Bounded Context Upstream │
│ (Affiliates) │
│ │
│ Affiliate │
│ Plan │
│ AffiliateStatus │
│ AffiliateOutputPort │
└──────────────────────────┘
| Componente | Responsabilidad |
|---|---|
| Facade (ACL class) | Orquesta la interacción con el contexto externo y devuelve objetos del dominio propio. |
| Translator | Convierte conceptos del dominio propio al lenguaje del contexto externo (y viceversa). |
| Application Port | Define el contrato en términos del dominio downstream, sin exponer detalles del upstream. |
El proyecto modela un escenario de salud o seguros donde el contexto de Autorizaciones necesita verificar si un afiliado tiene cobertura para un beneficio determinado. Sin embargo, el contexto de Afiliados tiene su propio modelo con conceptos como Plan, AffiliateStatus y Affiliate, que no pertenecen al lenguaje del dominio de autorizaciones.
Sin la capa anticorrupción, el servicio de autorizaciones tendría que:
// MAL: el dominio de Authorizations conoce y depende de conceptos de Affiliates
Affiliate affiliate = affiliateRepository.findByDocument(doc);
if (affiliate.getStatus() == AffiliateStatus.ACTIVE) {
Plan plan = affiliate.getPlan();
if (plan.includes("BEN-" + benefitCode)) { ... }
}El contexto de autorizaciones ahora habla el idioma de afiliados: sabe qué es un Affiliate, qué significa ACTIVE, cómo se estructura un Plan y cómo se codifican los beneficios con el prefijo "BEN-". Cualquier cambio en el modelo de afiliados rompe directamente el dominio de autorizaciones.
El ACL encapsula toda esta traducción:
1. El puerto define el contrato en lenguaje propio (VerifyCoverage):
public interface VerifyCoverage {
Coverage verify(Document document, Benefits benefits);
}El dominio de autorizaciones solo conoce Document, Benefits y Coverage — conceptos propios de su Ubiquitous Language.
2. El Translator convierte entre lenguajes (BenefitsTranslator):
public class BenefitsTranslator {
public static String toBenefits(final Benefits benefits) {
return "BEN-" + benefits.getCode();
}
}La regla de mapeo "BEN-" + code queda aislada en un único punto. Si el contexto de afiliados cambia su convención de códigos, solo este translator necesita actualizarse.
3. La fachada ACL orquesta e integra (AffiliatesVerifyCoverageAntiCorruption):
public class AffiliatesVerifyCoverageAntiCorruption implements VerifyCoverage {
private final AffiliateOutputPort affiliateOutputPort;
@Override
public Coverage verify(final Document document, final Benefits benefits) {
String documentCode = document.getValue();
Affiliate affiliate = affiliateOutputPort.findByDocument(documentCode);
if (affiliate == null)
return new Coverage(false, "affiliate not found!");
if (!affiliate.isActive())
return new Coverage(false, "affiliate inactive");
boolean include = affiliate.hasBenefits(BenefitsTranslator.toBenefits(benefits));
if (!include)
return new Coverage(false, "No includes plan");
return new Coverage(true, "Valid Coverage");
}
}Este componente es el único lugar donde los dos modelos se encuentran. Consume objetos de affiliates y devuelve objetos de authorizations. El dominio downstream nunca se contamina.
src/main/java/
├── affiliates/ # Bounded Context: Affiliates (upstream)
│ ├── application/
│ │ └── AffiliateOutputPort.java # Puerto de salida
│ └── domain/
│ ├── Affiliate.java # Entidad raíz
│ ├── AffiliateStatus.java # Value Object (enum)
│ └── Plan.java # Entidad de dominio
│
└── authorizations/ # Bounded Context: Authorizations (downstream)
├── application/
│ └── VerifyCoverage.java # Puerto - contrato en lenguaje propio
├── domain/
│ ├── Benefits.java # Value Object propio
│ ├── Coverage.java # Value Object de resultado
│ └── Document.java # Value Object propio
└── intrastructure/
├── AffiliatesVerifyCoverageAntiCorruption.java # Fachada ACL
└── BenefitsTranslator.java # Traductor de conceptos
| Escenario | Aplica ACL |
|---|---|
| Integración con sistema legacy que no puedes modificar | Si |
| Consumo de un API externo con modelo de dominio diferente | Si |
| Comunicación entre Bounded Contexts con relación Customer-Supplier | Si |
| Dos módulos del mismo equipo con el mismo Ubiquitous Language | No |
| Prototipo o MVP donde la velocidad es prioridad sobre la integridad | No (aún) |
El ACL no existe en el vacío. Se relaciona con los Context Mapping patterns definidos por Eric Evans:
- Conformist: el downstream acepta el modelo del upstream tal cual. El ACL es la alternativa cuando eso no es aceptable.
- Open Host Service / Published Language: el upstream expone un modelo genérico. El ACL puede simplificar o especializar ese modelo para el downstream.
- Shared Kernel: ambos contextos comparten una porción del modelo. El ACL es preferible cuando el acoplamiento del Shared Kernel es demasiado costoso.
- Separate Ways: los contextos no se integran. El ACL es para cuando la integración es necesaria pero se quiere minimizar el acoplamiento.
- El ACL vive en la infraestructura del contexto downstream, no en el upstream. Es el consumidor quien se protege.
- El puerto (
VerifyCoverage) es una interfaz del dominio/aplicación, lo que permite sustituir la implementación sin afectar las capas superiores. - El Translator es un componente separado de la fachada, respetando el Single Responsibility Principle: la fachada orquesta, el translator convierte.
- Los Value Objects del dominio downstream (
Document,Benefits,Coverage) encapsulan conceptos propios sin referencia alguna al modelo de afiliados.
