Anti-Corruption Layer (ACL) Pattern in DDD
Fast recall
Quick revision
Core answer
When integration corrupts a domain model, how the ACL protects it, and concrete implementation (adapters + translators).
Skipping the ACL because 'it's just a wrapper'; the foreign model leaks via 'temporary' direct usage that becomes permanent.
Discuss ACL as a Hexagonal driven adapter, the cost of building one, and when *not* to (e.g., trusted internal partner with shared kernel).
Interview-ready guide
Interview answer
The corruption problem
When integrating with an external system, two options:
Option 1: adopt their model. Their Customer has 47 fields you don't need, their customer_type enum is NA-OLD-3, their order status overlaps yours with different lifecycle states. Schema changes break your code. Your codebase reads in their language.
Option 2: build an Anti-Corruption Layer. Translate at the boundary; keep your domain pure. Small adapter is the only place that imports their classes. Exposes operations in your language. Translates fields, statuses, semantics. When they change, you change the ACL — not the rest of your code.
Option 1 always loses long-term. ACL is the price of integrating with systems you don't control.
Architecture
Port (CrmGateway) in your domain layer, speaks your language. Adapter implements the port; lives in ACL. Translator classes do field-level conversion. Foreign model imported only inside the ACL package.
Code example
Domain: CrmGateway interface with findCustomer(CustomerId), recordContact(CustomerId, ContactType). Customer record with id, name, email, tier.
ACL adapter: LegacyCrmGatewayAdapter implements CrmGateway. Uses LegacyCrmClient + LegacyCustomerTranslator. findCustomer fetches LegacyCustomer, maps via translator. recordContact maps your ContactType enum to legacy 'PH-1', 'EM-NEW', 'IP-V2' codes.
Translator: maps LegacyCustomer (custIdent, firstNm, lastNm, emlAddr1, tierCd) to domain Customer. Maps tier code 'T1-OLD'/'T1-NEW' → GOLD, 'T2' → SILVER, default → BRONZE.
Use cases see CrmGateway + Customer — your language. Legacy CRM types contained inside ACL.
When to use
- Integrating with legacy system with different model/vocabulary
- Integrating with third-party API (Stripe, Salesforce, SAP) shaped by their concerns
- Integrating with another team's BC with different model
- Strangler-fig migration during transition
- Foreign model unstable — frequent breaking changes
When to skip
- Two contexts share a Shared Kernel (small, stable, agreed-upon vocabulary; only with trusted internal partners)
- Integration is trivial and read-only
- Both teams agree on a Published Language (Avro/Protobuf) and model is close enough
Even in skip cases ask: when their model changes, who pays? If you do, want at least a thin translator.
ACL in strangler-fig migrations
New service uses its own clean domain model. When it needs data the legacy owns (customers, orders, products), goes through the ACL. ACL translates legacy quirks into new domain's vocabulary.
As ownership moves to new service, ACL shrinks; eventually disappears when legacy decommissioned.
Without ACL, new service inherits all legacy vocabulary and ages prematurely.
Common mistakes
- 'Just for now' — importing legacy model directly; temporary becomes permanent
- Translation logic in use cases — use case looks up legacyCustomer.getTierCd() and maps inline; ACL bypassed
- Returning legacy model from ACL — must return your domain types only
- No tests on the translator — where bugs live; legacy enum changes break domain silently
- One ACL for many foreign systems — each deserves its own with its own translator
- ACL bloat — grows business logic over time; keep it pure translation
Recommendation
Always build an Anti-Corruption Layer when integrating with a system you don't control — legacy, third-party, or another team's BC with different vocabulary. Place a clean port in your domain language, implement it in an adapter, hide the translator behind it. Foreign model imports stay contained inside the ACL package. Test the translator thoroughly with real fixtures — it's where breakage lurks. Skip the ACL only when you have a stable shared kernel with a trusted partner or a trivial read-only integration. The cost of writing an ACL is far less than the cost of a foreign model spreading through your codebase.
Anti-Corruption Layer (ACL) Pattern in DDD
Direct Answer
An Anti-Corruption Layer (ACL) is a translation layer placed between your bounded context and an external system (legacy, third-party, or another team's BC) to prevent the foreign model from leaking into yours. It exposes a clean interface in your domain language, hides the foreign system's quirks (different vocabulary, different semantics, mismatched data shapes), and is the only place that knows the foreign model. Use it whenever you integrate with a system you don't control — a legacy ERP, a third-party CRM, or a team that uses different terminology. Without an ACL, the foreign model corrupts yours.
Common Mistakes & Pitfalls
Skipping the ACL because 'it's just a wrapper'; the foreign model leaks via 'temporary' direct usage that becomes permanent.
Check your understanding
Answer these without looking back at the article.
- Walk through using an ACL during a strangler-fig migration off a legacy monolith.
- What's the difference between an ACL and a Shared Kernel?
- How do you test the translator in an ACL?
- When does an ACL evolve into business logic, and how do you push that back?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.