skip to content

Concretely, what are the moving parts you'd build when implementing an Anti-Corruption Layer between a new service and a legacy backend, and how does a request flow through them?

level: middleimportance: must knowfreq 60%

answer

  1. facade -> adapter -> translator
  2. adapter = plumbing/protocol
  3. translator = semantic mapping
  4. one ACL per external system typical
  5. fail loudly on unmapped legacy values

basics

~20 s

You build three parts: a front door that speaks your new system's language, a connector that knows how to call the old system, and a converter that maps data between the two. A request goes in the front door, gets converted, sent to the old system via the connector, and the response gets converted back.

solid answer

~40 s

A typical ACL implementation has a facade exposing operations in the new system's own domain language, one or more adapters that handle the actual protocol/transport details of the legacy or external system (REST, SOAP, raw SQL, message queue), and translator/mapper components that convert payloads field-by-field between the legacy shape and the new domain model in both directions. A request from a consumer hits the facade, the facade delegates to the adapter to fetch legacy data, the translator converts that into the new domain object, and the facade returns it - the consumer never sees the legacy shape. For writes, the same flow runs in reverse: the new domain object is translated into whatever the legacy system expects before the adapter sends it.

go deeper

for a junior

Should be able to describe, at a high level, that a request passes through a translator before reaching the new system's code, even without precise component names.

for a middle

Should name the facade/adapter/translator roles and trace a read request end-to-end through them, including what crosses each boundary.

for a senior

Should discuss granularity decisions (per-consumer vs. per-system ACL), how writes flow in reverse, and how translators are tested and monitored for unmapped/drifted values.

for a principal

Should reason about async/event-based ACL variants, ownership boundaries across teams, and how the component design affects the eventual cost of retiring the legacy system.

## A cluster of components, not one class An Anti-Corruption Layer is not one class but a small cluster of cooperating components, and understanding the flow of a single request through them is the fastest way to reason about where things can go wrong. ## The facade The outermost piece is the **facade** (sometimes called a gateway or port): an interface phrased entirely in the new system's own domain language, for example `InventoryLookup.getStockLevel(sku: Sku): StockLevel`. Consumers on the clean side of the boundary only ever call this interface: - they never import a legacy client library, - never see a legacy DTO, - and never have to know which transport protocol is used underneath. This is the contract that makes the boundary enforceable: as long as every call to the legacy system is forced to go through the facade, no legacy concept can leak in by accident. ## The adapters Behind the facade sit one or more **adapters**, which own the actual mechanics of talking to the foreign system: - opening a SOAP client, - issuing a raw SQL query against a decades-old database, - calling a vendor's REST API with its particular auth scheme, - or consuming a message off a legacy queue in its native format. The adapter's job is purely plumbing - retries, connection pooling, timeouts, protocol-specific error handling - and it returns data still shaped the way the legacy system produced it (a legacy DTO, an XML document, a raw row set). ## The translator Between the facade and the adapter sits the **translator** (or mapper), which does the actual semantic work: converting the legacy shape into the new domain model, field by field. This is where real judgment calls live - deciding: - that a legacy status code maps to `OrderStatus.CANCELLED`, - that a legacy nullable string field means 'unknown' rather than 'zero', - that a flat legacy record needs to be split into two related domain objects, - or that a missing legacy field needs a default or a lookup elsewhere to be filled in. Translators are usually the most heavily unit-tested part of the ACL, precisely because they encode business judgment about ambiguous or messy legacy data, and because their correctness is invisible unless you specifically write tests exercising the legacy system's edge cases (nulls, legacy-only codes, malformed dates). ## Tracing one request Tracing one read request: a consumer calls `facade.getOrder(orderId)`; the facade delegates to the adapter, which issues whatever legacy call is needed (say, a SOAP request) and gets back a legacy `OrderRecordDTO`; the translator converts that DTO into the new system's `Order` domain object, resolving any mismatches along the way; the facade returns the clean Order to the consumer, who never sees the SOAP envelope or the legacy field names. For a write, the flow reverses: the consumer passes a domain Order to the facade, the translator converts it into the shape the legacy system expects (which may mean re-deriving legacy-only fields the new model doesn't carry), and the adapter sends it through the legacy protocol. ## The same three roles, on messages In an asynchronous or event-driven integration, the same three roles appear but operate on messages instead of request/response calls: an adapter subscribes to the legacy system's native event stream (e.g. a legacy queue emitting XML), a translator converts each message into the new system's event schema, and a thin publishing facade republishes the translated event onto the new system's bus, so every downstream consumer only ever sees the new format regardless of how many upstream legacy quirks were absorbed in translation. ## Granularity: one per system, or one per consumer A design decision teams have to make early is granularity: should there be one ACL per external system, or one per bounded context that consumes it? | Shape | Upside | Downside | |---|---|---| | A single shared ACL | less duplicated effort | risks becoming a monolith that every team fears touching | | A per-consumer ACL | more isolated | duplicates translation logic if several teams need overlapping data | Most teams land on one ACL per external system, owned by whichever team is closest to that integration, exposing a facade generic enough for multiple internal consumers. ## Where translation failures should surface A second decision is where translation failures should surface. A well-built translator should **fail loudly** - throwing a typed error or emitting a metric - when it meets legacy data it doesn't recognize (an unmapped status code, an unexpected null), rather than silently defaulting to a 'best guess' value, because silent defaults are exactly what let bad legacy data quietly corrupt the new domain model days or weeks before anyone notices. This is why production-grade ACL implementations typically pair their translators with contract tests against the legacy system's actual response shapes and with monitoring on 'unmapped value' events, treating the translator as a first-class piece of business logic rather than plumbing.

  • Should the translator ever call back into the legacy system to enrich data, or should it be a pure function?
    Ideally a translator is a pure mapping function over the data the adapter already fetched, which keeps it easy to unit test without hitting the legacy system. If enrichment genuinely requires another legacy call (e.g., resolving a foreign key to a name), that call should happen in the adapter layer and be handed to the translator as already-fetched data, rather than having the translator itself reach out, so the translator stays deterministic and testable.
  • How would you test the translator component in isolation?
    Feed it a representative set of real (or realistically captured) legacy payloads - including edge cases like nulls, legacy-only status codes, and malformed dates - and assert on the resulting domain object, with no network calls involved since the adapter is mocked out or not invoked at all. Golden-file tests built from captured production payloads are common, since they catch drift if the legacy system's actual output shape changes.
  • What's a reasonable way to decide whether the ACL should be its own deployed service versus an in-process library?
    If only one service consumes the legacy system, an in-process library avoids an extra network hop and deployment unit. If multiple independently-deployed services need the same translation, or the legacy protocol needs specialized long-lived connections (e.g. a persistent mainframe session), a standalone ACL service avoids duplicating that complexity in every consumer.

Like an embassy: the facade is the front desk speaking your language, the adapter is the courier who physically travels into the host country to fetch documents, and the translator is the person who converts those documents into your language before anyone in the building reads them.

saying these in an interview costs you the question

  • Describes the ACL as a single undifferentiated blob with no separation of concerns
  • Thinks the adapter should also contain the domain-mapping logic
  • Says translation failures should just default silently to avoid errors
  • Can't describe how a write (not just a read) flows through the layers
  • Assumes the ACL must always be synchronous request/response

context