When a new service needs to integrate with a legacy system that has messy, inconsistent naming and data structures, what problem does putting an Anti-Corruption Layer between them solve?
answer
- translation facade
- foreign model quarantine
- facade + adapters + translators
- prevents legacy leak
- paired with strangler fig
basics
~10 sAn Anti-Corruption Layer is a translator between two systems. It converts the old system's weird data and terms into clean data your new system understands, so the mess doesn't spread into your new code.
solid answer
~30 sAn Anti-Corruption Layer (ACL) is a translation facade at the boundary between a new system and a legacy or external one. It converts requests and responses between the two models so the new system's domain code never has to know the legacy system's field names, status codes, or quirks. Without it, those foreign concepts leak into the new codebase and every consumer becomes coupled to legacy shapes and bugs. The ACL isolates that translation in one place, so the new domain model stays clean while the legacy system keeps its own internal shape untouched.
go deeper
Should be able to say, in plain terms, that an ACL is a translation boundary that keeps a legacy system's mess out of the new code, and give a rough real-world analogy.
Should be able to name the concrete pieces (facade, adapter, translator/mapper) and explain, with a small example, how a request crosses the boundary and gets converted.
Should discuss the maintenance and latency cost of the layer, and describe at least one production failure mode (leaky translation, translation drift) they'd guard against with tests or contract checks.
Should place the ACL in a broader modernization strategy (e.g. strangler fig migration), discuss when the cost isn't justified, and describe how ownership and lifecycle of the layer is managed as the legacy system is eventually retired.
## Two mental models to reconcile Every integration between two systems has to reconcile two different mental models: the vocabulary, data shapes, and invariants each side uses to represent the same real-world concepts. When two systems are built by different teams, at different times, or joined through a merger or acquisition, their models rarely agree - one side might: - call a customer a 'party', - encode status as a three-letter legacy code instead of an enum, - represent money as an untyped string with an implicit currency. An **Anti-Corruption Layer (ACL)** is a dedicated translation boundary - typically implemented as a facade backed by adapters and mapper/translator components - placed between the 'clean' new system and the 'foreign' legacy or external one. Every inbound and outbound call crosses through this facade, which converts the foreign representation into the new system's own domain model (and back again), so that no legacy-specific concept, naming, or invariant ever appears inside the new codebase. ## The moving parts Mechanically, the ACL is usually built from three cooperating pieces. 1. A **facade** or gateway interface exposes operations phrased in the new system's own ubiquitous language (e.g. `getOrder(orderId): Order`), so consumers on the clean side never see the legacy contract directly. 2. Behind that facade sit **adapters** that know how to actually talk to the legacy system - a SOAP client, a raw JDBC connection to an old database, a screen-scraper, or a vendor SDK. 3. Between the facade and the adapters sit **translators** (mappers) that convert the legacy DTOs, field-by-field and rule-by-rule, into the new system's domain objects, resolving mismatches such as different enumerations, units, null semantics, or missing fields that must be defaulted or looked up elsewhere. In an asynchronous or event-driven setting, the same idea applies to message payloads: an ACL can subscribe to a legacy system's event stream, translate each message into the new system's event schema, and republish it, so downstream consumers only ever see the new format. ## Why the pattern exists The reason this pattern exists is to protect the integrity of the new system's domain model. If a team lets legacy concepts leak straight through - passing legacy status codes into business logic, embedding legacy field names in APIs, or copying legacy validation quirks - the corruption spreads: - every consumer of the new system now has to understand and defensively code around the legacy system's history, - any future change to the legacy system (or its eventual retirement) becomes a shotgun-surgery exercise touching dozens of call sites. Concentrating the translation logic in one boundary means the **blast radius** of a legacy quirk, bug, or format change is confined to the ACL's mappers, and the rest of the new system can be designed, tested, and reasoned about using only its own clean model. ## The trade-off The trade-off is real cost on the other side of that ledger. - **Extra code.** The ACL is extra code that has to be written, tested, and kept in sync with both models as they evolve - a genuine double-maintenance burden, since a schema change on either side can silently break a translator. - **An extra hop.** It also adds a network or in-process hop, which shows up as latency and, at scale, as another component that needs its own monitoring, retries, and failure handling. - **A second legacy system.** If a team is not disciplined, the ACL itself can accrete unrelated business logic over time and become a second legacy system - a shared, poorly-owned service that nobody wants to touch, defeating its own purpose. ## Production failure modes Production failure modes cluster around a few recurring shapes. 1. The most common is a **'leaky' ACL**, where under time pressure a legacy field, status code, or naming convention is passed through untranslated because 'we'll fix it later' - and it never gets fixed, so the corruption the layer was built to prevent happens anyway, just more slowly. 2. A second is **translation drift**: the legacy system's API or database schema changes (a new status value, a renamed column) and the translator silently maps it incorrectly instead of failing loudly, producing subtly wrong domain objects that surface as confusing bugs far downstream. 3. A third is **performance degradation** from naive translation - for example, calling the legacy system once per item instead of batching, turning a single logical operation into an N+1 storm across the ACL boundary. ## A worked example, and where the pattern comes from A concrete, well-documented example is a **strangler-fig-style migration**: a company migrating an old on-premises ERP to a new cloud-native order-management service builds an ACL as a small facade service that calls the ERP's SOAP endpoints, translates its idiosyncratic status codes and XML shapes into the new service's `Order` and `StockLevel` domain types, and exposes only that clean contract to the new microservices. This exact use case - protecting a new system from a legacy or third-party model during incremental modernization - is why cloud architecture guidance (such as Microsoft's Azure Architecture Center) catalogs Anti-Corruption Layer as one of its cloud design patterns, distinct in emphasis from (but historically rooted in) its original definition as a Domain-Driven Design pattern for managing relationships between bounded contexts.
- Does the ACL have to be a separate deployed service, or can it be a library inside the consuming application?It can be either - the pattern only requires that translation logic be isolated behind a facade, not that it run in its own process. For a single consuming service, an in-process module or library with a facade interface and mapper classes is often enough and avoids an extra network hop. A standalone service makes sense when multiple independent consumers need the same translation, or when the legacy protocol (e.g. mainframe screen-scraping) needs specialized infrastructure that shouldn't be duplicated in every consumer.
- How is an Anti-Corruption Layer different from a plain API gateway or a generic adapter pattern?A generic adapter typically just changes an interface's shape (e.g., method signatures) without necessarily protecting a whole domain model, and a gateway usually focuses on routing, auth, and cross-cutting concerns rather than semantic translation. An ACL is narrower in intent: its job is specifically to prevent a foreign domain model's concepts from leaking into a protected domain model, which is why it always includes deliberate mapping/translation logic, not just protocol or routing changes.
- What happens if you skip the ACL and integrate directly against the legacy system's API?Every consumer ends up coupled to the legacy system's naming, status codes, and bugs, so a later legacy change or retirement requires touching every call site instead of one boundary. It also makes the new domain model harder to keep clean, since developers under deadline pressure will often just pass legacy shapes straight through rather than modeling them properly.
Like a diplomatic interpreter at a summit: each side keeps speaking its own language, and the interpreter translates in both directions so neither side's phrasing or idioms contaminate the other's understanding.
saying these in an interview costs you the question
- Can't explain what leaks into the domain if there's no ACL
- Thinks the ACL is only about changing method names/HTTP paths
- Assumes ACL must always be a separate microservice
- No mention of the extra latency/maintenance cost
- Confuses this pattern with a generic API gateway with no translation logic