Skip to content

Anti-Corruption Layer#

The anti-corruption layer (ACL) is a translation boundary between your clean domain model and an external system (legacy DB, third-party API, partner SaaS) that has different concepts and rules. The ACL adapts foreign data into your domain language so nothing alien leaks into the core.

flowchart LR
  Core[Your domain<br/>Order, Customer] --> ACL[Anti-corruption layer<br/>translates both ways]
  ACL --> Foreign[Legacy CRM<br/>Account, SaleHeader]
  Foreign --> ACL --> Core

    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    class Core service;
    class ACL edge;
    class Foreign external;

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;

Use it whenever the cost of speaking a foreign language inside your domain is higher than the cost of an adapter.

Eric Evans introduced the ACL as a strategic DDD pattern. It protects your bounded context from another bounded context whose model you don't control.

Where the ACL sits#

flowchart TB
  subgraph Bounded[Your bounded context]
    App[Domain services] --> Port[OutboundPort<br/>e.g. CrmGateway]
    Port --> ACL[Anti-corruption layer]
  end
  ACL --> Foreign[(External system<br/>different model)]

    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    class App,ACL service;
    class Port edge;
    class Foreign external;

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;

The domain depends on a port that uses domain types. The ACL implements that port, calls the foreign API, and translates between vocabularies.

A concrete example#

Your domain has Customer. The legacy CRM exposes Account with twelve fields, two of which are useful.

// Domain (clean)
data class Customer(val id: CustomerId, val email: Email, val tier: Tier)

interface CustomerLookup {
    fun byId(id: CustomerId): Customer?
}

// ACL implementation
class CrmCustomerLookup(private val crm: LegacyCrmClient) : CustomerLookup {
    override fun byId(id: CustomerId): Customer? {
        val acct: LegacyAccount = crm.getAccount(id.value) ?: return null
        // translate
        return Customer(
            id = CustomerId(acct.accountId),
            email = Email(acct.primaryContactEmail),
            tier = when (acct.salesGroup) {
                "GOLD" -> Tier.PREMIUM
                "SILVER" -> Tier.STANDARD
                else -> Tier.FREE
            }
        )
    }
}

The domain never sees LegacyAccount. If the CRM is replaced, only CrmCustomerLookup changes.

Two-way translation#

ACLs translate in both directions:

Direction Example
Inbound CRM Account → domain Customer
Outbound Domain Order → ERP SalesDocument payload

Don't conflate the two; they often need different code paths and tests.

ACL vs facade vs adapter#

Pattern Concern
Adapter Interface-level translation; same vocabulary
Facade Simplification; same model
ACL Vocabulary translation across model boundaries

A DatabaseAdapter over JDBC is an adapter. A wrapper that simplifies a noisy library is a facade. A thing that converts CRM concepts into your domain language is an ACL.

Strangler fig migration#

ACLs are the bedrock of the strangler fig pattern - gradually replacing a legacy system:

flowchart LR
  C([Client]) --> R[Router / proxy]
  R -->|legacy paths| L[Legacy monolith]
  R -->|new paths| New[New service]
  New --> ACL[ACL] --> L

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;

The new service talks to the legacy system via an ACL. Over time, more functionality moves into the new service; eventually the legacy can be removed.

Implementation tips#

  • Translate at the edges, not throughout. The ACL is a single boundary; the rest of the new code is clean.
  • Version the foreign data shape. Keep DTO classes in the ACL package, not in the domain.
  • Test the translation logic explicitly. Use known foreign payloads and assert domain shape.
  • Cache stale, not fresh. The ACL is often the right place for response caches that hide foreign latency.

When you don't need an ACL#

  • The foreign system speaks the same language (your other microservice).
  • The foreign system is short-lived; you'll throw the integration away.
  • The cost of a clean domain is higher than the value (one-off script).

Where ACL fits#

flowchart TB
  ACL((Anti-corruption<br/>layer))
  HX[Hexagonal architecture<br/>structural home]
  DDD[DDD tactical<br/>bounded contexts]
  SP[Structural patterns<br/>adapter / facade cousins]
  RP[Repository pattern<br/>different boundary]
  HX --> ACL
  DDD --> ACL
  SP -. cousins .- ACL
  RP -. distinct .- ACL

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class ACL service;
    class HX,DDD,SP,RP datastore;

Glossary & fundamentals#

Tag Concept What it is Page
LLD DDD tactical the parent strategic pattern set ddd-tactical
LLD Hexagonal architecture the structural home of ACLs hexagonal-architecture
LLD Structural patterns adapter and facade siblings structural-patterns
LLD Repository pattern a different boundary; ACL is the translation layer repository-pattern

Quick reference#

What#

Translation boundary between your clean domain and a foreign model (legacy, third-party, partner).

Goal#

Foreign concepts must NEVER leak into your domain.

Two-way mapping#

  • Inbound: foreign → domain
  • Outbound: domain → foreign

ACL vs adapter vs facade#

Pattern Concern
Adapter Interface translation, same model
Facade Simplify, same model
ACL Vocabulary translation across models

Strangler fig#

New service calls legacy via ACL → migrate slice by slice → retire legacy.

Implementation tips#

  • DTOs live in the ACL package, never in domain
  • Test translation explicitly with sample payloads
  • Cache stale data in the ACL if foreign latency is high
  • Version foreign DTOs for graceful migration

When unnecessary#

  • Foreign system speaks same domain
  • Throwaway integration
  • Cost of clean domain > benefit

Refs#

  • Evans - DDD strategic chapter
  • Microsoft Azure architecture - ACL pattern
  • Fowler - Strangler Fig

FAQ#

What is an anti-corruption layer?#

A translation boundary between your domain model and a foreign system, so external concepts never leak in. The layer adapts data both directions and protects the integrity of your core model.

When should I introduce an ACL?#

When integrating with a legacy database, partner SaaS, or vendor API whose vocabulary clashes with your domain. Without an ACL, foreign concepts spread through your code as accidental complexity.

How is an ACL different from a plain adapter?#

An adapter changes the interface shape. An ACL also translates the model and enforces invariants, ensuring that no foreign type, field, or rule reaches your application services.

Where do I put an ACL in a layered architecture?#

At the boundary, typically in the infrastructure layer of a hexagonal design. The domain depends only on ports; the ACL is the adapter that speaks the foreign language.

What is the cost of an ACL?#

Extra translation code and tests, plus a small CPU overhead per call. Most teams accept this cost in exchange for keeping the core model clean and replaceable.

Further reading#