Anti-corruption layer
How the anti-corruption layer pattern translates between a legacy system's domain model and your modern domain. Its role in DDD bounded contexts, the types of translation it performs, and when it prevents domain contamination.
TL;DR
- The anti-corruption layer (ACL) is a translation boundary between a legacy system's domain model and your new domain model.
- Without it, legacy concepts (abbreviated field names, magic integers, flat data structures) leak into your clean domain and make it look like the old system.
- The ACL is the only place in your codebase that knows about both domain models. All other code uses only your language.
- Translation is bidirectional: incoming legacy data maps to your model before entering your system; outgoing calls convert your model back to legacy format.
- In Domain-Driven Design (DDD) terms, the ACL is used when integrating with a downstream context you cannot change and do not want to conform to.
The Problem
You're building a new service that needs to call a 15-year-old ERP system. The ERP uses its own data model: TXN_HDR for order headers, CUST_REC for customers, STAT_CD as a two-digit integer for status. Some of these integer codes haven't been documented since 2011.
If you let the ERP model leak into your new code, you absorb its problems. Your Order class starts carrying fields named TXN_HDR_NO. Your team has to look up what STAT_CD=7 means every time they read a test. Your new system starts to smell like the old one, and the migration has accomplished nothing.
This is domain contamination. Once it sets in, it is very hard to reverse. Any future attempt to replace the ERP becomes a re-migration of your "new" system too.
One-Line Definition
The anti-corruption layer is a translation boundary between a legacy or foreign domain model and your own, so your domain stays clean regardless of what the external system looks like.
Analogy
Think of a professional interpreter at a diplomatic meeting. The diplomat on one side speaks only English; the official on the other speaks only Mandarin. The interpreter doesn't just translate words, they translate concepts and implied meanings. "We should table this item" means something completely different in British English than in American English; the interpreter knows which meaning applies.
The ACL is your interpreter. It translates not just field names but semantics: STAT_CD=3 in the ERP means "pending manual review" in your domain, not just "status three." The interpreter is the only person in the room who knows both languages. Everyone else speaks their own.
Solution
The ACL sits at the boundary of integration. Every piece of data that crosses from the external system into your domain passes through the ACL. Every outgoing call from your domain to the external system passes through it too.
Your service layer never sees legacy data structures. The translator is the only code that knows field abbreviations, magic status codes, or the ERP's date integer format. When the ERP eventually changes its field naming convention, you update one file.
What Translation Looks Like
Legacy systems commonly carry several problems at once. Here is the full catalog and how the ACL handles each one.
Abbreviated field names: CUST_NO, TXN_DT, ACCT_BAL_AMT. The ACL renames them to human-readable identifiers in your model.
Status codes as magic integers: STAT_CD=3 means "pending review." The ACL decodes them into expressive enums. The magic number appears exactly once, inside the ACL.
Dates as integers: Many legacy systems store dates as YYYYMMDD integers or as days since a fixed epoch. The ACL parses them into proper date objects before they enter your domain.
Flat structures mixing concerns: One 50-column table per entity, joining order, customer, and line items in a single row. The ACL reshapes the structure into proper domain objects.
from decimal import Decimal
from datetime import date
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
PENDING_REVIEW = "pending_review"
SHIPPED = "shipped"
DELIVERED = "delivered"
UNKNOWN = "unknown"
class LegacyOrderAdapter:
"""ACL: translates legacy TXN_HDR records into clean Order domain objects."""
_STATUS_MAP = {
1: OrderStatus.PENDING,
2: OrderStatus.CONFIRMED,
3: OrderStatus.PENDING_REVIEW, # magic number decoded once, here only
4: OrderStatus.SHIPPED,
5: OrderStatus.DELIVERED,
}
def to_domain(self, row: dict) -> "Order":
return Order(
id=OrderId(row["TXN_NO"]),
customer_id=CustomerId(row["CUST_NO"]),
status=self._STATUS_MAP.get(row["STAT_CD"], OrderStatus.UNKNOWN),
created_at=self._parse_yyyymmdd(row["TXN_DT"]),
total=Money(
amount=Decimal(row["TOTAL_AMT"]) / Decimal("100"),
currency=Currency(row["CURR_CD"])
)
)
def to_legacy(self, order: "Order") -> dict:
reverse_status = {v: k for k, v in self._STATUS_MAP.items()}
return {
"TXN_NO": str(order.id),
"CUST_NO": str(order.customer_id),
"STAT_CD": reverse_status.get(order.status, 0),
"TXN_DT": int(order.created_at.strftime("%Y%m%d")),
"TOTAL_AMT": int(order.total.amount * 100),
"CURR_CD": order.total.currency.code,
}
def _parse_yyyymmdd(self, value: int) -> date:
s = str(value)
return date(int(s[:4]), int(s[4:6]), int(s[6:8]))
Notice that to_legacy() is just as important as to_domain(). The ACL handles both directions. When you write an order status update back to the ERP, the ACL converts OrderStatus.PENDING_REVIEW back to 3. The ERP never knows your model changed.
The ACL must never leak legacy types
If any legacy type (a raw dict with ERP field names, a generated ERP client class) escapes the ACL into your domain code, the layer has failed. Add type annotations and code review rules to catch this. Your domain methods should only accept and return your own domain objects.
ACL in Domain-Driven Design
In Domain-Driven Design (DDD), a bounded context is a cohesive area of a system with its own domain model and ubiquitous language. When two bounded contexts integrate, their models need an explicit relationship.
Eric Evans defined several context map patterns for these relationships. The ACL is the one you use when the upstream model is legacy or messy and you want to protect your domain:
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.