Menu

Anti-Corruption Layer

Protect your clean domain model from legacy or external systems: translation layer, facade pattern, and bounded context boundaries.

10 min read

The Problem: When External Models Pollute Yours

Every system eventually integrates with something ugly: a legacy ERP system with a cryptic data model, a third-party payment gateway with its own notion of a 'transaction', or an acquired company's API that uses entirely different terminology. If you map their concepts directly into your clean domain model, their mess leaks in and corrupts your abstractions. This is where the Anti-Corruption Layer (ACL) comes in.

The ACL is a term from Domain-Driven Design (DDD), introduced by Eric Evans. It is a translation layer that sits between your bounded context and a foreign system. It converts the foreign system's data structures and concepts into your own domain language, so your domain model stays clean regardless of what is on the other side.

ACL Architecture

Loading diagram...
The ACL contains facades, translators, and adapters that shield your domain from foreign models.

The ACL is composed of three internal building blocks:

  • Facade — Simplifies the external system's interface, hiding its complexity from your domain.
  • Translator — Converts data structures and concepts between the two models (e.g., `ExternalOrder` → `PurchaseOrder`).
  • Adapter — Handles protocol or technology differences (e.g., SOAP → REST, XML → JSON).

Concrete Example: Payment Gateway Integration

Imagine your domain model has a `Payment` entity with fields like `amount`, `currency`, `customerId`, and `status`. Your payment gateway (say, Stripe) uses its own concepts: `PaymentIntent`, `charge`, `metadata`, and status codes like `requires_payment_method`. Instead of coupling your domain to Stripe's model, you build an ACL:

typescript
// Your clean domain model
interface Payment {
  id: string;
  customerId: string;
  amount: Money;
  status: "pending" | "completed" | "failed";
}

// Anti-Corruption Layer: translates Stripe's model to yours
class StripeACL {
  async charge(payment: Payment): Promise<PaymentResult> {
    // Translate your domain model → Stripe's API format
    const intent = await stripe.paymentIntents.create({
      amount: payment.amount.toCents(),        // Stripe uses integer cents
      currency: payment.amount.currency.toLowerCase(),
      metadata: { customerId: payment.customerId },
    });

    // Translate Stripe's response → your domain model
    return {
      success: intent.status === "succeeded",
      paymentId: intent.id,
      domainStatus: this.translateStatus(intent.status),
    };
  }

  private translateStatus(stripeStatus: string): Payment["status"] {
    const map: Record<string, Payment["status"]> = {
      succeeded: "completed",
      requires_payment_method: "pending",
      canceled: "failed",
    };
    return map[stripeStatus] ?? "failed";
  }
}

ACL vs Other Integration Patterns

PatternPurposeWhere Translation Happens
Anti-Corruption LayerShield your domain from a foreign modelAt the boundary of your bounded context
AdapterConvert interfaces (structural)Single class / component
FacadeSimplify a complex subsystemSingle class exposing simplified API
Open Host ServiceExpose your domain to others via a published protocolExternal boundary (you control it)
ℹ️

ACL in Microservices

In microservices, every service-to-service integration is a potential ACL boundary. When Service A calls Service B, it should translate B's response into its own domain language rather than passing B's data structures through its own domain. This keeps bounded contexts truly bounded.

When to Use an ACL

  • Integrating with a legacy system that has a poor or inconsistent data model.
  • Consuming a third-party API (payment gateways, shipping providers, CRM systems).
  • Integrating with an acquired company's systems during post-merger consolidation.
  • Crossing a bounded context boundary in a DDD system where the upstream team's model diverges from yours.
💡

Interview Tip

Interviewers rarely ask directly about ACLs, but they appear implicitly in questions like 'How would you integrate with a legacy payment system?' or 'How do you avoid coupling your microservices together?'. Mentioning that you would introduce a translation layer (ACL) to prevent the upstream model from leaking into your domain shows DDD knowledge and architectural maturity. Name-drop Eric Evans if appropriate.

⚠️

Don't Overengineer Small Integrations

If you are integrating with a single well-designed REST API with a stable contract, a simple adapter class may suffice. A full ACL is warranted when the external model is genuinely different from yours, when it changes frequently, or when you integrate with multiple providers behind the same interface.

📝

Knowledge Check

4 questions

Test your understanding of this lesson. Score 70% or higher to complete.

Ask about this lesson

Ask anything about Anti-Corruption Layer