Integration DDD

Anti-Corruption Layer

A translation layer between bounded contexts that prevents the concepts and semantics of an external or legacy system from contaminating your domain model.

⏱ 9 min read

What it is

An Anti-Corruption Layer (ACL) is an architectural pattern introduced by Eric Evans in Domain-Driven Design that places an explicit translation and isolation layer between two bounded contexts — particularly between a new, well-designed domain model and a legacy or external system with a different, often problematic model. The ACL ensures that the concepts, language, and constraints of the external context cannot bleed into the internal domain model. Instead of letting the legacy system's anachronistic entity names, data types, and business rules infect new code, the ACL acts as a firewall that translates foreign concepts into domain-native ones.

Mechanically, an ACL is typically composed of three collaborating components: a Facade that presents a simplified, stable interface to the external system; an Adapter that calls the external system using its native protocol and data structures; and a Translator (or set of translators) that converts the external model to and from the internal domain model. In a microservices context, the ACL is often an entire adapter service that sits between two services and owns the translation responsibility for that boundary.

Why it exists

Legacy enterprise systems accumulate decades of design decisions that were reasonable at the time but are now impediments: customer IDs are encoded as 10-digit strings with embedded type prefixes, "order status" is an integer code whose meaning is documented only in a 1998 mainframe manual, and "product" conflates the physical SKU, the pricing tier, and the promotional bundle in a single flat record. If your new domain service directly imports these concepts, it will carry those accidental complexities forever. When the legacy system changes, breakage propagates into your domain logic.

The ACL provides freedom to evolve independently. The internal domain model can use clean, expressive types — CustomerId as a typed value object, OrderStatus as an explicit enum, Product as a properly separated aggregate — while the ACL handles all the grotesque mapping. When the legacy system is eventually replaced, only the ACL changes; the domain model and all business logic built on it remain intact.

When to use

  • Integrating with a legacy system whose data model uses outdated, inconsistent, or poorly named concepts that you don't want mirrored in your new service.
  • Consuming a third-party vendor API (payment gateway, shipping carrier, CRM) where the vendor's model differs from your ubiquitous language.
  • Migrating a monolith to microservices incrementally: the strangler fig pattern carves out new services while the ACL bridges the new service to the legacy monolith's database or API.
  • Two bounded contexts within the same organization that have diverging ubiquitous languages and cannot agree on a shared kernel.
  • High-risk interfaces where a subtle semantic mismatch could cause business logic errors (e.g., currency units, timezone handling, status code interpretation).
  • When you need to swap the external dependency in the future without cascading changes to domain logic.

When not to use

  • The external context uses the same ubiquitous language as your domain — the translation layer is overhead without benefit; use a Conformist or Shared Kernel relationship instead.
  • Extremely simple integrations with one or two fields — an informal mapping in the service is cleaner than a full ACL abstraction.
  • When the external system is temporary (e.g., a migration bridge that will disappear in 3 months) — the investment in a formal ACL may not be justified.
  • Real-time, high-performance paths where every translation microsecond matters — consider caching or denormalized views instead.

Typical architecture


  Your Domain                ACL                    Legacy System
  ┌───────────────┐    ┌─────────────────┐    ┌─────────────────┐
  │               │    │   Facade        │    │                 │
  │  OrderService │───▶│  LegacyOrder    │───▶│  SOAP Endpoint  │
  │               │    │  Repository     │    │  /getOrder?     │
  │  Order        │    │                 │    │  CUST_NO=0012345│
  │  (rich model) │    │   Translator    │    │                 │
  │               │◀───│  toLegacy()     │◀───│  XML Response:  │
  │  Customer     │    │  fromLegacy()   │    │  …     │
  │  (value obj)  │    │                 │    │  07   │
  │               │    │   Adapter       │    │  USD   │
  └───────────────┘    │  (HTTP client,  │    └─────────────────┘
                        │   XML parsing) │
                        └─────────────────┘
          

Pros and cons

Pros

  • Domain model stays clean and expressive, free from legacy naming and structural constraints.
  • Replacing or upgrading the external system only requires updating the ACL, not domain logic.
  • The ACL is a natural seam for testing: domain logic can be unit tested against mock translators; integration tests focus on the ACL boundary.
  • Explicit translation surfaces subtle semantic mismatches (units, timezones, status codes) that would otherwise be hidden bugs.
  • Enables incremental strangler-fig migrations by decoupling the migration timeline from the domain evolution timeline.

Cons

  • Additional layer of code to write, test, and maintain; can feel like boilerplate for simple cases.
  • Translation may be lossy if the external model contains information that has no representation in the domain model.
  • Performance overhead of translation on every call; caching can help but adds complexity.
  • The ACL must evolve as both the domain model and the external system evolve — it can become a maintenance burden if the external system changes frequently.
  • Teams may underinvest in the ACL (treating it as "just plumbing"), allowing it to accumulate business logic that belongs in the domain service.

Implementation notes

Implement the ACL as a package or module that owns all knowledge of the external system. The domain layer should depend only on domain interfaces (e.g., OrderRepository, CustomerGateway) defined in the domain or application layer. The ACL module provides concrete implementations of those interfaces and imports all legacy-specific types, XML/SOAP libraries, or vendor SDKs. Nothing in the domain package should import anything from the ACL package — the dependency flows inward. This is the same rule as Ports and Adapters / Hexagonal Architecture.

For complex legacy systems, write comprehensive translator tests with real sample payloads from the external system captured in test fixtures. Use contract testing tools like Pact to verify the ACL remains compatible with the actual external system's behavior over time. When the external system returns error codes, translate them to domain exceptions with meaningful names rather than letting the HTTP status or SOAP fault propagate. Consider adding metrics and circuit breaker logic in the ACL adapter to contain failures from the external system.

Common failure modes

  • Leaky ACL: External system types (DTOs, error codes, SDK objects) are used directly in domain services instead of being translated, causing the external model to contaminate domain logic.
  • Business logic in the ACL: Developers add conditional logic ("if legacy status = 07 AND region = US, then treat as shipped") in the translator instead of encoding it as domain rules.
  • Missing inverse translation: The ACL translates inbound messages but forgets to handle outbound commands properly, causing writes to the external system with incorrect field values.
  • No error translation: HTTP 400 from the legacy system propagates as a generic exception to the domain, hiding information about what actually went wrong.
  • Stale fixtures: The external system silently changes its API or payload structure; the ACL's unit tests use hardcoded fixtures and pass, while production integration fails.

Decision checklist

  • Does the external system's domain language conflict with your ubiquitous language?
  • Are there semantic mismatches in key fields (status codes, currencies, units of measure)?
  • Do you need to replace or swap the external system in the future without changing domain logic?
  • Is this integration worth the investment in a formal translation layer vs. inline mapping?
  • Have you defined the domain interfaces that the ACL will implement?
  • Do you have a contract testing strategy to catch external API changes?

Example use cases

  • Strangler-fig migration: A team extracts an Order microservice from a monolith. The ACL wraps the monolith's SOAP-based order API, translating between the new domain model and the legacy DB schema until the monolith's order module can be decommissioned.
  • Payment gateway integration: A checkout service integrates with Stripe and PayPal. Each gateway has its own model for PaymentIntent, Charge, and Dispute. An ACL per gateway maps these to the domain's neutral PaymentResult type, so the checkout service is unaware of which gateway was used.
  • ERP product catalogue sync: An e-commerce platform syncs product data from SAP. The ACL translates SAP's "Material Master" records (with MARA, MAKT, MVKE tables) into the platform's clean Product aggregate, handling unit-of-measure conversions and status code mapping.
  • Canonical Data Model — The ACL often translates to/from a canonical schema rather than directly between two proprietary formats.
  • Bounded Contexts — The ACL is a DDD pattern for managing relationships between bounded contexts.
  • Change Data Capture — CDC feeds can be wrapped in an ACL so downstream consumers see clean domain events rather than raw DB change records.

Further reading