2026-05-13
When your clean domain model has to talk to a legacy system, a third-party API, or a partner service with bizarre conventions, the foreign model will leak into yours. Field names like cust_id_v2, nullable booleans, status codes encoded as strings ("01", "02", "99") — these alien concepts spread through your codebase until your domain is shaped by someone else's bad decisions. The Anti-Corruption Layer (ACL) is a translation boundary that keeps the rot contained.
An ACL is a dedicated module — usually a set of adapters, translators, and façades — that sits between your bounded context and the external system. Internally, your code speaks your domain language. The ACL converts to and from the foreign model at the edge. Nothing else in your codebase imports the foreign types.
Real-world example: An e-commerce team integrates with a 1990s-era ERP for inventory. The ERP returns:
STK_QTY as a space-padded string (" 42")STAT_CD where "A" means active, "D" deleted, "H" on holdWithout an ACL, every service that touches inventory ends up calling parseInt(stk_qty.trim()) and writing if (stat_cd === "A") checks. Six months later, the ERP team renames STAT_CD to STATUS_CODE and you have 47 places to update.
With an ACL, you have one ErpInventoryAdapter that returns a clean InventoryItem { quantity: number, status: "active" | "deleted" | "on_hold", priceCents: number }. The ERP rename touches one file.
When to build one:
Rule of thumb: If you find yourself importing foreign types in more than 2-3 places outside the integration module, you need an ACL. If a foreign concept appears in your domain layer (not just infrastructure), you've already lost — refactor before it spreads further.
Watch out: An ACL is not just a DTO mapper. A real ACL enforces your invariants — it rejects or normalizes data that violates your domain rules. If the ERP returns negative quantities, the ACL decides whether to throw, clamp to zero, or log and skip. That decision belongs at the boundary, not scattered across consumers.
