The Anti-Corruption Layer Pattern in ASP.NET Core: When to Use It and How

Every enterprise codebase eventually develops a problem that no design pattern can fully prevent: the gradual spread of someone else's model into your own. It might be a legacy ERP system whose data structures creep into your domain entities, a third-party payment provider whose error codes start appearing in your business logic, or a microservice whose contracts begin shaping decisions that should belong purely to your bounded context. The anti-corruption layer (ACL) pattern exists precisely to stop this contamination at the boundary β and in ASP.NET Core applications, applying it thoughtfully is one of the highest-leverage architectural decisions a team can make.
For those who want to see how the ACL fits into a complete production codebase β alongside repository abstractions, domain events, and CQRS handlers β Patreon has the full annotated implementation, with each layer wired together the way enterprise teams actually build it.
Understanding the ACL in isolation is useful, but seeing how it integrates into a broader clean architecture β how it interacts with MediatR pipelines, application services, and infrastructure adapters β is what makes the pattern click in practice. That's the core focus of Chapter 11 of the Zero to Production course, which covers Clean Architecture and CQRS together with the boundary patterns that make them durable under real-world pressure.
What Problem Does the Anti-Corruption Layer Solve?
In Domain-Driven Design (DDD), a bounded context is a logical boundary inside which a particular domain model is internally consistent and authoritative. The moment your bounded context starts consuming concepts from another context β or worse, another system entirely β without translating them, the foreign model begins polluting yours.
This contamination rarely happens dramatically. It begins with a small convenience: mapping a third-party API response directly into a domain object, or letting a database column name from an external schema drive how you name a property in your core entity. Each step feels harmless. Cumulatively, they create a domain model that no longer reflects your business β it reflects your dependencies.
The ACL pattern solves this by sitting at the boundary between two systems or bounded contexts and owning the translation responsibility entirely. Your domain model only ever sees concepts expressed in its own language. The ACL handles the mapping, the impedance mismatch, and the version drift.
The Three Roles the ACL Plays
Understanding the ACL means distinguishing three distinct responsibilities it can hold, even though in practice they often appear together:
Translation. The ACL converts concepts from an external model into concepts that make sense within your domain. A third-party payment system might return a transaction_state field with values like SETTLED, DECLINED, PENDING_REVIEW. Your domain should never know those strings exist. The ACL translates them into a PaymentStatus value object that belongs entirely to your model.
Isolation. The ACL ensures that changes in the external system β a renamed field, an added enum value, a restructured response shape β are absorbed at the boundary and never ripple through your domain. Your handlers, your application services, and your domain entities remain stable when external contracts change.
FaΓ§ade. In some designs, the ACL also simplifies a complex or inconsistent external interface, presenting a clean and intention-revealing surface to the rest of the application. Rather than exposing all the noise of an external SDK, the ACL exposes only what your domain needs β nothing more.
When to Use the Anti-Corruption Layer Pattern
The ACL is not a universal default. It adds a translation layer that has a real cost: more code, another indirection point, and another place where mapping bugs can hide. Apply it when the benefit of isolation outweighs that cost.
Use the ACL when:
You are integrating with an external system whose domain model you do not control and cannot change (payment gateways, ERP systems, third-party SaaS APIs, legacy databases).
You are consuming another bounded context within a microservices architecture whose model differs meaningfully from your own β even if both are owned by your organisation.
You are incrementally migrating away from a legacy monolith using the Strangler Fig pattern. The ACL lets the new system consume the old system's data without adopting its model.
The external interface is unstable, versioned, or likely to change β and you want to absorb those changes without touching domain logic.
You are implementing Clean Architecture or DDD and want to enforce the dependency rule: your domain layer should have zero awareness of infrastructure concerns.
Do not use the ACL when:
Both sides of the boundary share the same domain model (a shared kernel, in DDD terms). Introducing a translation layer here adds noise without benefit.
The external model is simple, stable, and maps cleanly to yours β a thin DTO-to-domain mapper already does the job.
Your team is small, the integration is shallow, and the overhead of a full ACL implementation exceeds the complexity it would prevent.
You are creating a generic "mapping layer" as an architectural ritual rather than to solve a real boundary problem. The ACL is not a substitute for AutoMapper.
How the ACL Fits Into ASP.NET Core's Clean Architecture Layers
In a standard Clean Architecture layout for ASP.NET Core, the layer boundaries are clear: Domain at the centre, Application around it, Infrastructure at the outside, and the API project as the entry point. The ACL sits in the Infrastructure layer β not because it is infrastructure itself, but because it is the translation mechanism between infrastructure concerns and domain concepts.
The interface that defines what the ACL exposes lives in the Application layer (or Domain layer, depending on how strict your dependency rule is). The implementation lives in Infrastructure. This keeps the dependency rule intact: your application never depends on the concrete external system β only on the abstraction you defined.
In practical terms, this means an IPaymentGatewayAdapter interface in Application, and a StripePaymentGatewayAdapter (or BraintreePaymentGatewayAdapter) in Infrastructure that depends on the Stripe SDK and translates its responses into domain-level types. When you swap payment providers, you swap the Infrastructure implementation. The Application and Domain layers never notice.
This structure maps directly to how the Clean Architecture starter template on GitHub separates concerns β the boundary between Application and Infrastructure is exactly where the ACL lives in a well-structured .NET solution.
Core Concepts You Need to Get Right
Translation Is the ACL's Only Job
The ACL should not make business decisions. If a translation requires domain logic β "if the external system returns status X, but the customer is in region Y, treat it as Z" β that logic belongs in a domain service or a policy object, not in the ACL itself. The ACL maps; it does not decide.
Incoming vs Outgoing Translation
ACLs are often thought of only in terms of incoming data (translating external responses into domain types), but they work equally well in reverse. When your domain issues a command that must be communicated to an external system, the ACL translates the domain command into the external system's expected format. Both directions belong inside the same boundary component.
What Belongs Behind the ACL
Not everything needs an ACL. The pattern earns its overhead when:
The external model is structurally incompatible with yours (different naming, different granularity, different lifecycle semantics).
The external system is versioned and you want to absorb breaking changes without modifying application code.
Multiple external systems could satisfy the same role β and you want to be able to swap them without touching domain logic.
What Does Not Belong Behind the ACL
Simple value conversions (string to enum, decimal to Money) β these belong in value object constructors or mapping helpers, not a full ACL.
Persistence mapping β this is the responsibility of EF Core's Fluent API configuration, not the ACL.
Validation of incoming requests β this belongs in the application layer's request validation pipeline.
What the ACL Looks Like in Practice
The shape of an ACL in ASP.NET Core follows a consistent pattern regardless of what you are integrating with:
Define the interface in Application. The interface expresses what your application needs in domain terms β not what the external system provides.
Implement the adapter in Infrastructure. The implementation depends on the external SDK, calls its API, and maps the response into domain types.
Register via DI. The interface is registered in the DI container pointing to the Infrastructure implementation. The Application layer resolves the interface; it never sees the concrete class.
Translate at the boundary. All mapping between external types and domain types happens inside the adapter implementation, never in handlers, services, or domain entities.
The interface itself should read like domain language. A method like Task<PaymentResult> AuthoriseAsync(PaymentRequest request, CancellationToken ct) tells you everything about what your domain needs and nothing about which payment provider implements it. The adapter's AuthoriseAsync implementation is the only place that knows about Stripe's ChargeService, Braintree's transaction objects, or whatever external SDK is in play.
Trade-offs Every Team Should Weigh
| Dimension | What You Gain | What It Costs |
|---|---|---|
| Domain stability | External changes are absorbed at the boundary | More code to write and maintain |
| Testability | Mock the interface; test domain logic without the external SDK | Mapping code needs its own tests |
| Replaceability | Swap external providers by swapping one Implementation class | Initial setup time |
| Clarity | Domain code reads in domain language throughout | Another layer of abstraction to understand |
| Complexity | Isolated complexity at the boundary | Mapping bugs can be subtle and hard to spot |
The trade-off is cleanest when the external system is complex, unstable, or shared across multiple application layers. It is least justified when the integration is simple and unlikely to change.
Anti-Patterns to Watch For
The leaking ACL. The ACL's translation is incomplete, and external types still appear in domain or application code. A common sign: your application layer imports a NuGet package that belongs to an external SDK. The ACL's job was to make that unnecessary.
The ACL as a God class. One adapter class that handles all integrations β payment, notifications, identity, shipping β becomes a coordination nightmare. Each integration should have its own focused adapter.
Business logic in the ACL. If your adapter starts making decisions based on the external system's response ("if the charge fails with code 4002, retry with a different card token"), it has crossed from translation into business logic. Extract that logic into a domain service.
Skipping the interface. Injecting the concrete adapter directly, without an interface, eliminates the testability and replaceability benefits. The interface is not optional ceremony β it is the mechanism that makes the pattern work.
Premature ACL. Creating an ACL for a simple, stable REST API that maps cleanly to your model is over-engineering. Apply the pattern where boundary complexity actually exists, not as a default for every external call.
Integration with Other Patterns
The ACL does not stand alone β it works most naturally alongside several patterns that are common in enterprise .NET:
Repository Pattern. If your ACL is translating data from a legacy database or external data store, it often pairs with a repository interface in Application. The ACL handles the translation; the repository handles the query abstraction.
CQRS with MediatR. MediatR handlers in the Application layer call ACL interfaces to fetch or send data. The handler never knows which external system is backing the interface. This separation, explored in depth in the CQRS and MediatR guide on Coding Droplets, is one of the most effective ways to keep application logic clean as integrations multiply.
Domain Events. When the ACL receives a response that should trigger domain behaviour, it should not trigger that behaviour itself. It should return a domain result, and the handler or domain service that calls it is responsible for raising domain events. Mixing domain event publication into the ACL couples translation to side-effects in ways that are hard to test and reason about.
Clean Architecture. The ACL is one of the primary mechanisms that keeps Clean Architecture's dependency rule intact under real-world integration pressure. Without it, Infrastructure concerns inevitably leak into Application and Domain layers. For a full picture of how these patterns compose in a production codebase, the Domain-Driven Design guide on Coding Droplets walks through how bounded context design drives where ACLs are needed.
Decision Guide: Do You Need an ACL?
Use this checklist before adding an ACL:
[ ] Does the external system have a domain model that differs meaningfully from mine?
[ ] Do I need to be able to swap or mock this external system without touching domain or application logic?
[ ] Is the external contract unstable, versioned, or likely to change?
[ ] Would external types appearing in my domain or application layer be a smell I want to prevent?
[ ] Is there more than one system that could fill this role, and do I want the flexibility to swap?
If you answered yes to 3 or more: add the ACL. If you answered yes to 1β2: evaluate whether a simple mapper handles it. If you answered yes to 0: a direct DTO mapping is fine.
β Prefer a one-time tip? Buy us a coffee β every bit helps keep the content coming!
FAQ
What is the difference between an anti-corruption layer and an adapter pattern? They serve related but distinct purposes. The adapter pattern solves an interface incompatibility β it makes two interfaces work together. The ACL solves a domain model incompatibility β it prevents a foreign model from corrupting yours. An ACL often uses the adapter pattern internally as its implementation mechanism, but the adapter pattern alone does not guarantee that translation happens in a way that protects your domain model's integrity.
Does the anti-corruption layer belong in the Domain layer or the Infrastructure layer? The interface belongs in the Application layer (or Domain layer if the consuming code is domain logic). The implementation belongs in the Infrastructure layer. This follows the Clean Architecture dependency rule: higher layers define what they need through interfaces; lower layers implement them. Putting the implementation in Infrastructure ensures the domain never depends on the external SDK.
Is an anti-corruption layer the same as an API gateway? No. An API gateway is an infrastructure component that handles routing, authentication, and rate limiting at the network level. An ACL is an in-process (or in-service) code-level translation mechanism that isolates your domain model from external models. You might have both: the gateway at the network boundary and the ACL at the code boundary.
When should I remove an anti-corruption layer? When the system it protects against no longer exists or has been fully replaced. If you applied the ACL during a Strangler Fig migration and the legacy system has been fully decommissioned, the ACL can be dissolved β your domain now speaks directly to the new system's model, which you presumably designed to align with yours. Never remove the ACL while the legacy system is still in use; that is precisely when it is earning its keep.
Can one ACL translate between two internal bounded contexts, or is it only for external systems? It is appropriate for both. In a microservices architecture, two services owned by the same organisation but designed around different bounded contexts may still need an ACL between them if their models differ meaningfully. The pattern is about model incompatibility, not system ownership.
How do I test an anti-corruption layer? Test the adapter implementation with unit tests that provide sample external responses and assert that the resulting domain types are correct. Test the downstream application logic with mock implementations of the ACL interface β so the handler tests never touch the real external system. This separation, where the interface is the test boundary, is one of the most concrete benefits the pattern provides.
Does the anti-corruption layer add performance overhead? In practice, negligible. The translation work β mapping a JSON response or an SDK object into a domain type β is in-process and extremely fast. The actual performance cost is always in the network call to the external system, not the mapping inside the ACL. Do not let performance concerns dissuade you from applying this pattern where it is genuinely needed.






