The Real World is Messy
In a perfect world, every system you integrate with would use clean, well-designed models that align with your domain. In reality, you integrate with legacy mainframes from the 1980s, third-party payment APIs with bizarre field names, and partner systems that use completely different terminology.
If your clean Bounded Context directly calls a legacy system and maps its response directly into your domain objects, the legacy system's messy model starts leaking into your code. You end up with fields named CUST_ACCT_NO_7 in your otherwise clean domain model. This is called model corruption.
The Translation Layer
An Anti-Corruption Layer (ACL) is a protective barrier you build between your clean Bounded Context and a messy external system. It is a dedicated translation layer that speaks both languages.
Your Domain Model ←→ ACL ←→ Legacy System / External API
The ACL has two responsibilities:
1. Outbound: Translate your clean domain requests into whatever format the external system requires.
2. Inbound: Translate the external system's messy response back into your clean domain model.
# The legacy system returns something like this:
legacy_response = {
"CUST_ACCT_NO_7": "C-00123",
"CUST_NM_FULL": "Jane Smith",
"ACCT_BAL_CENTS": 150000,
"ACCT_STAT_CD": "A"
}
# The ACL translates it into your clean domain model:
class LegacyAccountingSystemACL:
def get_customer_account(self, customer_id: str) -> CustomerAccount:
raw = self._legacy_client.fetch(f"CUST_ACCT_NO_7={customer_id}")
return CustomerAccount(
id=raw["CUST_ACCT_NO_7"],
name=raw["CUST_NM_FULL"],
balance=Money(raw["ACCT_BAL_CENTS"] / 100, "USD"),
status="ACTIVE" if raw["ACCT_STAT_CD"] == "A" else "INACTIVE"
)
Your domain code never sees CUST_ACCT_NO_7. It only ever works with CustomerAccount objects that use your Ubiquitous Language. The ACL absorbs all the ugliness.
The ACL is also the right place to handle differences in business concepts. If the legacy system calls something a "Client" but your domain calls it a "Customer," the ACL performs that conceptual translation too — not just field name mapping.