Anti-Corruption Layer Pattern

Anti-Corruption Layer Pattern

"An isolating layer to provide clients with functionality in terms of their own domain model. The layer talks to the other system through its existing interface, requiring little or no modification to the other system. Internally, the layer translates in both directions as necessary between the two models." — Eric Evans, Domain-Driven Design (2003), p. 365

Intent

The Anti-Corruption Layer (ACL) prevents a foreign system's model — its naming conventions, field structures, and semantic assumptions — from leaking into a domain's ubiquitous language. Without the ACL, integrating a legacy CRM or third-party API requires adopting their terminology inside the domain: domain entities start carrying fields like CUST_ID, ACCT_NO, and LEGACY_STATUS. The domain's ubiquitous language becomes corrupted by the external system's vocabulary.

The ACL is a translation boundary. It sits between the domain and the external system. All calls from the domain go through the ACL, which translates domain requests into the external system's protocol and translates responses back into domain objects. The external system sees no change to its interface. The domain sees only clean, domain-meaningful method signatures.

The ACL is the concrete implementation of the Anti-Corruption Layer relationship in the Bounded-Context Context Map. It is the recommended defensive strategy whenever a downstream context integrates with an upstream whose model is incompatible or uncontrollable.

Pitfall — ACL bypassed under time pressure: Teams skip the ACL "to save time" during legacy integration. Legacy model concepts bleed into the core domain, corrupting domain purity. The warning sign is unambiguous: legacy system field names such as CUST_ID, ACCT_NO, or LEGACY_STATUS appearing in domain entity constructors or domain service method signatures. This is the primary motivation for the ACL's name — the corruption is the legacy model's concepts, and the layer is the barrier that stops it.

When NOT to Use

  • When the external system's model is clean and semantically compatible with your domain — use Open Host Service or direct integration instead. The ACL adds maintenance overhead that is only justified when genuine model incompatibility exists.
  • When the integration is trivially simple: a single field lookup with no model translation needed does not warrant a full Facade + Translator structure.
  • When the overhead of maintaining the translation layer exceeds the domain purity benefit — rare, but applicable when the external system is being decommissioned imminently or the integration surface is a single, stable field.
  • When both systems share a Shared Kernel context map relationship — they already agree on the shared model; no translation is needed within the kernel boundary.

When to Use

  • Integrating with a legacy system that uses different terminology, field naming conventions, or data structures (the canonical use case — a legacy CRM with CUST_ID/ACCT_NO field names).
  • Integrating with a third-party API whose model concepts differ from your domain (e.g., a payment provider's concept of "Merchant" does not map cleanly to your domain's "Seller").
  • During a Strangler-Fig-Pattern migration where the legacy system coexists with the new domain model — the ACL prevents legacy model contamination during the transition period. The ACL is removed when the Strangler Fig migration is complete (legacy deleted, not merely bypassed).
  • Any integration where the upstream context map relationship is Conformist and your domain cannot afford to adopt the upstream model wholesale.
  • When letting the upstream model's concepts leak into your domain would corrupt your ubiquitous language — this is always the right call when genuine model mismatch exists.

How It Works

The ACL uses two collaborating components:

1. Facade (wraps the external system interface) Hides protocol details — SOAP, REST, proprietary SDK, database connection. The Facade exposes a simplified, stable interface to the ACL. If the external system changes its protocol (e.g., REST → gRPC), only the Facade needs to change. The Translator remains stable.

2. Translator (maps between external and domain models) Receives the external system's DTO or record format and converts it into domain entities and value objects. All legacy field names (CUST_ID, ACCT_NM, ACCT_BAL) are confined to the Translator. They never appear in domain entity constructors, domain service signatures, or anywhere outside the ACL boundary.

The ACL class combines both: it exposes domain-meaningful methods to the domain layer (findCustomer(id: string): Promise<Customer>), internally delegating to the Facade for data retrieval and the Translator for model mapping. From the domain's perspective, the ACL is simply a repository-like collaborator with a clean domain interface. The legacy system's existence is invisible.

Flow Diagram

flowchart LR
    subgraph Domain Layer
        DS[Domain Service]
        DE[Domain Entity]
    end

    subgraph ACL["Anti-Corruption Layer"]
        direction TB
        F[Facade]
        TR[Translator]
        F --> TR
    end

    subgraph Legacy System
        LS[Legacy API]
        LD[Legacy DTO<br/>CUST_ID, ACCT_NM]
    end

    DS -->|"findCustomer(id)"| ACL
    F -->|"HTTP/SOAP call"| LS
    LS -->|"Legacy DTO"| F
    TR -->|"map to domain model"| DE
    ACL -->|"Customer entity"| DS

    style ACL fill:#ffd,stroke:#333,stroke-dasharray: 5 5
    style Legacy System fill:#fdd,stroke:#933

TypeScript Example

// Anti-Corruption Layer — TypeScript (Facade + Translator composite)
// Source: Evans, Domain-Driven Design, p.365
interface LegacyCustomerDTO {
  CUST_ID: string;   // legacy field names confined to this interface
  CUST_NM: string;
  ACCT_BAL: number;
}
 
class CustomerTranslator {
  toDomain(dto: LegacyCustomerDTO): Customer {
    // CUST_ID / CUST_NM never leak past this method
    return new Customer(dto.CUST_ID, dto.CUST_NM, Money.create(dto.ACCT_BAL, 'USD'));
  }
}
 
class LegacyCrmFacade {  // Facade hides legacy HTTP/SOAP protocol
  async getCustomer(id: string): Promise<LegacyCustomerDTO> { /* legacy call */ return {} as any; }
}
 
class CustomerACL {  // Anti-Corruption Layer: Facade + Translator combined
  constructor(
    private facade: LegacyCrmFacade,
    private translator: CustomerTranslator
  ) {}
  async findCustomer(id: string): Promise<Customer> {
    const dto = await this.facade.getCustomer(id);
    return this.translator.toDomain(dto);  // legacy model never leaks into domain
  }
}

Java Example

// Anti-Corruption Layer — Java (Spring @Service wrapping legacy client)
// Source: Evans, Domain-Driven Design, p.365
// Legacy DTO fields (CUST_ID, ACCT_NM) are confined to the Translator — never leak into domain objects.
@Service
public class CustomerAcl {
 
    @Autowired
    private LegacyCrmClient legacyClient;  // legacy HTTP/SOAP client
 
    @Autowired
    private CustomerTranslator translator;
 
    public Customer findCustomer(String id) {
        LegacyCustomerDto dto = legacyClient.getCustomer(id);  // returns legacy DTO
        return translator.toDomain(dto);  // Translator confines legacy field names
    }
}
 
// CustomerTranslator.toDomain(dto) maps CUST_ID → customerId, ACCT_NM → name, etc.
// Domain Customer constructor never receives CUST_ID or ACCT_NM directly.

Lineage Backward

  • Facade-Pattern (GoF ancestor) — The ACL uses the Facade structural pattern to simplify access to the legacy system and hide its interface complexity. ACL extends Facade with translation semantics: the Facade hides the how (protocol, connection, SDK details); the ACL additionally transforms the what — converting foreign model concepts into domain concepts. A pure Facade adapts the interface; the ACL adapts the model.

Lineage Forward

  • Strangler-Fig-Pattern (Phase 14) — The Strangler Fig migration pattern uses an ACL during the migration period to prevent the new domain from being contaminated by the legacy system while both coexist. The ACL is removed when the Strangler Fig migration is complete (legacy deleted, not merely bypassed — bypassed is not done).
  • Bounded-Context (bidirectional) — The ACL implements the Anti-Corruption Layer context map relationship pattern described in Bounded Context. Every time a context map shows an Anti-Corruption Layer relationship, an ACL is the concrete mechanism enforcing that boundary.
PatternRelationship
Facade-PatternGoF structural ancestor — ACL uses Facade to simplify legacy system access; ACL adds model translation on top of structural simplification.
Bounded-ContextACL implements the Anti-Corruption Layer context map relationship. It is the boundary enforcement mechanism when upstream and downstream models are incompatible.
Strangler-Fig-PatternACL is the isolation mechanism during Strangler Fig migrations, keeping the legacy model from contaminating the new domain during the coexistence period.
Adapter-PatternAdapter is structural — it adapts an interface to match an expected contract. ACL is strategic — it adapts the model, translating concepts and semantics, not just method signatures.
  • Modular-Monolith — The ACL is the extraction mechanism in the Modular Monolith migration path; introduced before module extraction to translate between the module's internal model and the emerging external contract.

Sources