Anti-Corruption Layer
Protect your clean domain model from legacy or external systems: translation layer, facade pattern, and bounded context boundaries.
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
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:
// 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
| Pattern | Purpose | Where Translation Happens |
|---|---|---|
| Anti-Corruption Layer | Shield your domain from a foreign model | At the boundary of your bounded context |
| Adapter | Convert interfaces (structural) | Single class / component |
| Facade | Simplify a complex subsystem | Single class exposing simplified API |
| Open Host Service | Expose your domain to others via a published protocol | External 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.