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.
Related Topics#
- Hexagonal Architecture: the ACL is a specific kind of outbound adapter
- DDD Tactical: bounded contexts are why ACLs need to exist
- Structural Patterns: adapter and facade are cousins