skip to content

Your team's new 'Orders' microservice needs to read data from a 20-year-old legacy inventory system that uses cryptic status codes like 'S3' and denormalized flat records. Instead of letting the Orders service call the legacy API directly and pass those codes around internally, the team builds a small module that sits between them and converts everything into the Orders service's own clean model before anything else touches it. What is this module called, and what problem does it solve?

level: juniorimportance: must knowfreq 65%

answer

  1. translator at the seam
  2. bounded context protection
  3. one-time translation cost
  4. leaky ACL anti-pattern
  5. strangler fig companion

basics

~20 s

It's an anti-corruption layer - a translator sitting at the boundary that converts a foreign or messy system's data/language into your own clean model, so ugliness on the other side never leaks into your code.

solid answer

~50 s

An anti-corruption layer (ACL) is an isolating translation layer placed at an integration boundary. It exposes an interface in the consuming service's own domain language, and internally converts requests and responses to and from the upstream system's model - whether that's a legacy monolith, a third-party API, or another team's service built around a different bounded context. The goal isn't just data mapping; it's protecting the integrity of your domain model so foreign naming, status codes, or workflow assumptions never leak into your business logic. Without an ACL, every consumer of the legacy system becomes coupled to its quirks, and those quirks propagate outward - making the new service almost as hard to change as the old one it was meant to replace. The ACL absorbs that instability at a single, well-tested seam instead of scattering translation logic everywhere.

go deeper

for a junior

Should recognize the basic shape - a translation module at the boundary - and give a plausible reason (avoid messy legacy code spreading). Not expected to discuss bounded contexts by name or trade-offs in depth.

for a middle

Should articulate the domain-model-protection rationale explicitly, know where the ACL typically lives in a service's code, and mention that it needs its own tests.

for a senior

Should discuss the leaky-ACL failure mode from firsthand experience, know when to extract the ACL into its own service vs keep it embedded, and reason about maintenance cost/ownership.

for a principal

Should connect this to migration strategy (e.g., strangler fig), organizational ownership of the seam, and be able to argue for or against building one given the specific upstream's stability and expected lifespan.

## The two pieces it is built from Mechanically, an ACL is built from two pieces: - a **facade/interface** that speaks the consuming service's own domain language, and - a **translator** (sometimes split into an adapter for protocol/transport concerns and a translator for semantic/model mapping) that does the actual conversion to and from the upstream system's shapes. Say the Orders service defines a domain concept `StockLevel` with a clean enum `AVAILABLE/LOW/OUT_OF_STOCK`. The legacy inventory system returns flat records with a field `stat_cd` holding values like 'S1', 'S2', 'S3', plus a dozen other fields Orders doesn't care about (warehouse bay codes, a mainframe timestamp format, etc.). The ACL's translator is the only piece of code in the whole system that knows the mapping from 'S3' to `LOW`; everywhere else in Orders, code works exclusively with `StockLevel`. Calls into the legacy system, and only those calls, pass through this seam; the rest of the codebase never imports a legacy client library, never parses a legacy response body, and never reasons in legacy vocabulary. ## Why it exists This exists because **bounded contexts** (a core DDD idea) assume each service owns a model tailored to its own problem, and that model degrades the moment foreign concepts are allowed to seep in undigested. Integration is where model corruption happens fastest: a legacy system was built under different constraints, at a different time, often by a different team with different naming conventions, invariants, and failure semantics. If a new service adopts those shapes wholesale — passing legacy status codes through its APIs, storing legacy IDs as primary keys, or embedding legacy validation quirks into its own logic — it inherits legacy debt without inheriting legacy context. The ACL is the deliberate refusal to let that happen: it draws a hard line at the boundary and pays a one-time translation cost so everything inside stays coherent, testable, and independently evolvable. ## What it costs The cost is real and worth naming honestly. - Every field the upstream system can produce has to be **mapped**, which means writing and maintaining translation code that has no business value of its own — it doesn't ship features, it just insulates. - That code needs its **own tests** (often the most valuable tests in the integration, because they catch upstream schema drift before it reaches consumers). - There is also a **runtime cost**: an extra hop or transformation step adds latency, however small, and an extra failure surface — the translator can throw on unexpected values the legacy system emits that were never seen in testing. - And there's an **ownership cost**: someone has to keep the ACL current as both sides evolve, which is easy to underfund once the initial migration project is 'done' and the team moves on. ## Failure modes In production, ACL failures show up in a few recognizable shapes. 1. The first is the **leaky ACL**: a developer under deadline pressure passes an upstream field straight through 'just this once' because mapping it properly is inconvenient, and within a few sprints half the legacy vocabulary is back inside the domain model — the layer exists on paper but not in practice. 2. The second is **drift**: the legacy system adds a new status code or changes an enum's meaning, the ACL's mapping table doesn't cover it, and the translator either throws, silently defaults to a wrong value, or (worse) passes the unknown value through untranslated, corrupting downstream logic in a way that's hard to trace back to its source. 3. The third is the **ACL becoming its own legacy system**: over years it accretes special cases, becomes the only thing anyone still understands about how the two systems relate, and nobody dares touch it — exactly the brittleness it was built to prevent, just moved one layer over. ## Where it shows up A textbook real-world case is the **strangler fig** migration pattern: a team peeling functionality off a legacy monolith into new microservices puts an ACL in front of the monolith so each new service can be built against a clean model from day one, while the monolith itself is incrementally decommissioned behind that seam. Another common instance is integrating with an enterprise system like SAP or a mainframe order-management system, where the vendor's data model is fixed and verbose; teams write a dedicated adapter service (sometimes literally called an 'SAP ACL' or 'legacy facade') whose only job is exposing a small, well-named REST or event contract that hides the vendor's field names, unit conventions, and workflow states from every other service in the organization.

  • Where in the codebase should the ACL physically live - inside the consuming service, or as a separate deployable?
    Both are valid; it depends on how many consumers share the same upstream and how much translation logic there is. A single consumer with modest mapping usually embeds the ACL as an internal module/package to avoid an extra network hop. When multiple services need the same translated view of a legacy system, or the mapping logic is heavy, extracting it into its own small service avoids duplicating translation logic and centralizes the one place that has to change when the legacy system changes.
  • How would you test an ACL differently from the rest of the service?
    The translator itself deserves focused unit tests per mapping rule, including edge cases like unknown/unexpected upstream values, so schema drift is caught immediately rather than surfacing as a mystery bug three layers downstream. Contract tests against the upstream system's actual response shapes (or a recorded fixture of them) are also valuable, since the translator's correctness depends entirely on assumptions about a system you don't control.
  • What's the difference between an ACL and a plain DTO mapper?
    A DTO mapper is often just field-for-field data reshaping with no protective intent - it can still leak foreign semantics if the mapper reuses the source's enums or naming. An ACL is a deliberate architectural boundary: it enforces that the translated output is expressed entirely in the consumer's own domain vocabulary and invariants, and it's usually paired with an explicit decision about what NOT to expose from upstream at all.

Like a customs/immigration checkpoint at a border: goods and people from the other country get inspected, re-documented, and converted to local standards before they're allowed to move freely inside - nothing crosses untranslated.

saying these in an interview costs you the question

  • Says ACL and DTO mapper are the same thing
  • Can't explain why passing the legacy status code straight through is a problem
  • Assumes the ACL only needs to handle the response shapes seen during development
  • Thinks the ACL must always be a separate microservice
  • No mention of protecting the domain model, only 'converting formats'

context