Anti-Corruption Layer

A translation layer that protects your system's clean internal model from a messy or incompatible external system, so its quirks don't leak inward.

What is it?

Suppose your system needs to integrate with an old, external partner API that represents money as raw floating-point numbers, uses cryptic three-letter status codes like "PND" and "CMP", and occasionally returns a customer's name in an inconsistent format. It would be fast to just use that data structure directly throughout your own codebase — but doing so means every part of your system that touches customer or payment data now has to understand the partner API's quirks, and if you ever integrate a second partner (or that partner changes their API), the mess spreads even further.

An anti-corruption layer is a deliberate boundary — a small set of translation code — placed exactly at the seam where your system meets that external one. Its only job is to convert the external system's messy, foreign model into your own clean, well-defined internal model (using proper Money value objects, a clear PaymentStatus enum, a normalized customer name) — and to convert your outgoing data back into whatever shape the external system expects. Nothing outside that boundary ever sees the partner API's raw shapes at all.

The name is deliberately vivid: without this layer, the external system's design decisions — its bad naming, its inconsistent shapes, its legacy quirks — literally "corrupt" your own model by leaking into it. The anti-corruption layer is the thing standing in the way of that corruption spreading inward.

Explain like I'm 10

A country's customs and immigration checkpoint doesn't let foreign currency, foreign paperwork formats, or foreign legal standards flow straight into the domestic system untranslated — everything gets converted (currency exchanged, forms translated, standards checked) right at the border, so the mess of every different country doesn't leak into everyday domestic life.

Examples

Without an anti-corruption layer: the external shape leaks everywhere

// Partner API returns: { amt: 19.99, stat: "PND", cust_nm: "SMITH, JOHN" }
async function displayOrderStatus(orderId) {
  const raw = await partnerApi.getOrder(orderId);
  const isPending = raw.stat === "PND"; // magic string, understood everywhere it's used
  const dollars = raw.amt; // a float, error-prone for money math
  const name = raw.cust_nm.split(", ").reverse().join(" "); // parsing logic scattered wherever it's needed
  return { isPending, dollars, name };
}

Every function that needs order status, amount, or customer name has to independently know the partner's cryptic codes and inconsistent formats — and duplicate the parsing logic to deal with them.

With an anti-corruption layer: one place absorbs the mess

// acl/partnerOrderTranslator.js — the ONLY file that knows the partner's raw shape
function translatePartnerOrder(raw) {
  return {
    amount: new Money(Math.round(raw.amt * 100), "USD"), // proper value object
    status: raw.stat === "PND" ? "pending" : raw.stat === "CMP" ? "completed" : "unknown",
    customerName: normalizeName(raw.cust_nm), // "SMITH, JOHN" -> "John Smith"
  };
}

// Everywhere else in the app
async function displayOrderStatus(orderId) {
  const raw = await partnerApi.getOrder(orderId);
  const order = translatePartnerOrder(raw); // clean internal model from here on
  return order.status === "pending";
}

Only translatePartnerOrder knows the partner's raw field names and quirky codes. Every other part of the app works with a clean, self-consistent internal model (Money, a readable status string, a normalized name) regardless of how messy the source was.

How it works

An anti-corruption layer sits at the exact boundary between your system and the external one, exposing an interface shaped entirely around your internal model. Internally, it does whatever translation is required — renaming fields, converting units, mapping status codes, reformatting data, even reconciling structural differences (e.g., the external system splits into two calls what your model treats as one concept). If the external system changes its API, or you swap in a second, different external partner, only this translation layer needs to change — the rest of your codebase, which only ever spoke your internal model, doesn't need to know anything happened.

Why does it exist?

It exists because integrating with external systems — especially legacy ones, third-party vendors, or systems you don't control — inevitably means dealing with modeling decisions you'd never choose yourself. Without a deliberate boundary, those decisions creep into your own domain model piece by piece, until your "clean" internal model is quietly shaped by someone else's legacy constraints, and every future integration or vendor change becomes a wide, unpredictable blast radius instead of a contained one.

When to use it

Use one wherever your system integrates with an external API, legacy system, or another team's service whose model doesn't match — or you don't want to be permanently bound to — your own domain model. It's especially valuable when you might switch or add vendors later, or when the external system is known to be inconsistent, poorly designed, or likely to change.

When not to use it

If the external system's model is already clean, stable, and a reasonably good match for your own needs, adding a full translation layer is unnecessary indirection. It's also overkill for a quick prototype or a one-off integration you know won't be maintained or extended — the insulation an anti-corruption layer buys isn't worth much if there's nothing to protect long-term.

Common mistakes

  • Letting the external system's raw response objects leak past the translation layer 'just this once,' which reintroduces the exact coupling the layer exists to prevent.

  • Building the translation layer but shaping your internal model to closely mirror the external one anyway, gaining little real insulation.

  • Skipping the anti-corruption layer for a 'temporary' integration that ends up living in production for years, by which point removing the coupling is far more expensive.

Practice exercises

  1. Easy:

    Given a legacy API that returns dates as US-formatted strings ('MM/DD/YYYY'), write a small translation function that converts them into your internal model's Date objects.

  2. Medium:

    Design an anti-corruption layer for integrating with a third-party shipping API that uses different status codes and a different address format than your internal Order and Address models. List the fields you'd translate.

  3. Hard:

    Your company is switching from one payment processor to another, each with a completely different API shape. Explain how having an anti-corruption layer in place for the first processor changes the amount of work required to add the second one.

Interview questions

What is an anti-corruption layer?

A translation boundary placed between your system and an external system, converting the external system's model into your own clean internal model (and vice versa), so the external system's quirks and inconsistencies never leak into your core code.

Why not just use the external API's data shapes directly throughout your codebase to save time?

Because then every part of your codebase that touches that data has to understand the external system's quirks, and any change to the external API or a switch to a different provider forces changes throughout the entire codebase instead of in one isolated place.

How does an anti-corruption layer relate to dependency inversion?

Both isolate your core logic from a volatile external detail by placing a boundary between them; an anti-corruption layer is essentially dependency inversion and translation applied specifically at the seam with an external or legacy system whose model you don't control.