skip to content

A Billing bounded context needs data from a legacy Inventory system whose data model uses incompatible concepts (e.g., a single 'SKU status' enum that encodes both stock level and discontinuation). What pattern keeps that legacy model from leaking into and corrupting Billing's own domain model, and how does it work mechanically?

level: seniorimportance: must knowfreq 65%

answer

  1. translation at the boundary
  2. protects downstream model's purity
  3. adapter + facade combo
  4. isolates from legacy churn
  5. cost = extra code + decay risk

basics

~20 s

An Anti-Corruption Layer - a translation layer at the boundary that converts the legacy system's data and concepts into Billing's own clean model, so Billing's code never has to understand or depend on the legacy shape directly.

solid answer

~50 s

The Anti-Corruption Layer (ACL) is a dedicated translation component between Billing and the legacy Inventory system: adapters that call Inventory using its native protocol, paired with a facade that maps results into types expressed purely in Billing's own vocabulary - e.g., translating Inventory's overloaded "SKU status" enum into Billing's own explicit InStock/Discontinued distinction. Billing's domain code only ever talks to the ACL's clean interface, with zero direct dependency on Inventory's schema or quirks. This isolates Billing from Inventory's model churn and accidental complexity - if Inventory changes its enum values, or gets replaced entirely, only the ACL's translation code changes, not Billing's domain logic. The cost is the ACL itself: extra code to write and maintain, and the risk of building a passthrough that mirrors the upstream shape and provides no real protection if the team isn't disciplined.

go deeper

for a junior

Should get that there's a 'translator' component between two systems' different data shapes.

for a middle

Should explain that the translation protects the downstream domain model, not just that it converts field names.

for a senior

Should explain the mechanics (adapter/facade), name the trade-off (extra code/latency vs isolation), and identify when an ACL is actually warranted versus overkill (e.g., Conformist as the alternative).

for a principal

Should be able to reason about ACL placement in a system-wide integration strategy, including when to invest in an ACL for a system slated for replacement (temporary) versus a permanent integration point, and how to prevent ACL decay over time.

## The situation it is named for An **Anti-Corruption Layer (ACL)** is the mechanism DDD names for a specific, common situation: your bounded context needs data or behavior from another system - often a legacy system, a third-party vendor, or another team's poorly modeled context - whose concepts don't map cleanly onto your own domain model, and you need to consume it without importing its conceptual mess into your own code. ## The pieces it is made of Mechanically, an ACL sits as a distinct layer between your domain code and the external system, typically implemented as a combination of: - an **adapter** (or several) that speaks the external system's native protocol - calling its REST API, reading its database, parsing its file format; - a **facade** that presents a single, clean interface to the rest of your domain code; - between those two pieces, **translation logic** that converts the external system's data and concepts into types expressed entirely in your own domain's ubiquitous language. Concretely: if a legacy Inventory system encodes both current stock level and product discontinuation status into one overloaded 'SKU status' enum with values like `ACTIVE_LOW`, `ACTIVE_HIGH`, `DISCONTINUED_WITH_STOCK`, `DISCONTINUED_NO_STOCK`, the ACL's translator reads that raw enum and produces two independent, explicit concepts in Billing's own model - say, a `StockLevel` value object and a separate boolean `isDiscontinued` flag - because that's how Billing's own domain logic actually reasons about the business, and it should never need to understand or pattern-match on Inventory's historically-accumulated encoding scheme. ## Why it exists: protecting model integrity The reason this exists is to protect **model integrity**. Domain models earn their value from being clean, explicit expressions of the business concepts a team's code actually needs to reason about; if you let an external system's accidental complexity, historical quirks, and unrelated concerns leak directly into your model - by, say, mapping fields 1:1 with matching names and matching enum values - your own domain code inherits every ambiguity, workaround, and legacy assumption baked into the other system, and you lose the very benefit a well-modeled bounded context was supposed to provide. The ACL is the place where that translation and simplification happens explicitly and visibly, rather than being smeared invisibly throughout your business logic. ## What it costs and what it buys The trade-off cuts both ways. | Side of the ledger | What it means | |---|---| | **On the cost side** | an ACL is real code to write, test, and maintain - translator functions, adapters, and often a set of tests specifically asserting the mapping behaves correctly under the external system's various quirks and edge cases - plus, if implemented as a network-calling layer, a potential latency or availability overhead versus calling the external system directly | | **On the benefit side** | your domain model stays pure and independent of the external system's churn - if Inventory's team changes their enum values, or your company swaps the legacy Inventory system for a different vendor entirely, only the ACL's translation code needs to change; Billing's domain logic, business rules, and tests remain untouched, because they were never written in terms of Inventory's representation in the first place | ## The failure mode: decay into a passthrough The clearest production failure mode is **ACL decay into a thin passthrough**. Over time, under delivery pressure: 1. a translator that started as genuine conceptual translation gets replaced field-by-field with near-identical renames that mirror the upstream shape exactly; 2. upstream-specific concepts (like the overloaded status enum) start appearing directly inside downstream business logic or tests 'just this once'; 3. eventually every upstream schema change once again ripples deep into the downstream domain model - at which point the ACL exists in name only and provides none of its intended protection. Detecting this requires periodically checking whether the downstream model's types and vocabulary still read as native to the downstream domain, or have started to mirror the upstream system's naming and structure. ## When not to build one It's also worth knowing when not to build one: if the upstream system's model is already clean, stable, and conceptually well-aligned with your own domain - most commonly when integrating with a trusted internal team's well-designed **Open Host Service** - the translation overhead of a full ACL may not earn its cost, and the **Conformist** pattern (adopting the upstream model as-is) can be the more pragmatic choice. The decision to build an ACL, then, is really a judgment call about how much you distrust or diverge from the upstream model, weighed against the ongoing cost of maintaining the translation layer.

  • What's the difference between an Anti-Corruption Layer and just writing a mapper/DTO conversion function?
    A simple DTO mapper is often part of an ACL, but ACL is a broader architectural commitment: it implies the downstream context's domain model is deliberately designed as if the upstream system didn't exist, with translation treated as a first-class concern (including handling upstream inconsistencies, retries, and protocol differences), not just a one-line field rename. A thin DTO mapper that mirrors the upstream shape 1:1 isn't really protecting anything.
  • When would you NOT build an Anti-Corruption Layer even though you're integrating with an external system?
    When the upstream system's model is already clean, stable, and conceptually aligned with your context (e.g., integrating with a well-designed OHS/Published Language from a trusted internal team), an ACL's translation overhead may add cost without real protective value - sometimes called the Conformist pattern, where you simply adopt the upstream model as-is because negotiating or translating isn't worth it.
  • How would you detect that an Anti-Corruption Layer has decayed into a thin passthrough that no longer protects the domain?
    Signs include: the downstream domain types starting to mirror the upstream system's naming and structure field-for-field, upstream-specific concepts (like an overloaded status enum) appearing directly in downstream business logic or tests, and every upstream schema change requiring changes deep inside downstream domain code rather than being absorbed entirely within the translation functions.

An Anti-Corruption Layer is like a diplomatic interpreter at a summit between two countries with different languages and legal systems: the interpreter translates statements into your country's language and legal concepts in real time, so your side never has to directly adopt or be corrupted by the other country's idioms, ambiguities, or legal categories - you only ever operate within your own well-defined language, mediated at the border.

saying these in an interview costs you the question

  • can't distinguish an Anti-Corruption Layer from a generic mapper
  • thinks it means calling the legacy system less often (a caching concern, not a translation concern)
  • doesn't mention that the isolated domain model is written as if the legacy system didn't exist
  • believes it eliminates the need for the legacy system's API to be stable at all

context