Asynchronous Messaging Pattern :👈 👉:Identify-bounded-contexts-in-DDD

Anti-Corruption Layer (ACL) in Domain-Driven Design (DDD)

Anti-Corruption Layer (ACL) in Domain-Driven Design (DDD)

An Anti-Corruption Layer (ACL) in Domain-Driven Design (DDD) is a layer of code that protects your application's domain model from the data structures, business rules, and design of an external system or legacy application.

It acts as a translator between two different systems, ensuring that the external system's concepts do not directly affect your application's domain model.

1. Why do we need an Anti-Corruption Layer?

Imagine you are developing a new e-commerce application using ASP.NET Core and DDD.

Your application has its own domain model:

  • Customer
  • Order
  • Money
  • OrderStatus

However, you need to integrate with an old ERP system that uses different names and data formats.

For example:

Your application Legacy ERP system
CustomerId CUST_NO
OrderStatus ORD_STS
Shipped S
Cancelled X
Money Amount stored as a decimal without currency information

If your domain code directly depends on these legacy formats, the ERP system's design begins to influence your application.

An ACL translates the legacy concepts into your application's own domain concepts.

2. Architecture without an ACL vs. with an ACL

❌ Without an Anti-Corruption Layer

Your Domain Model
(Contains legacy field names and status codes)
          |
          | Direct dependency
          v
Legacy ERP System
(CUST_NO, ORD_STS, S, X)

Problem: changes in the ERP can spread into your domain logic.

✅ With an Anti-Corruption Layer

Your Domain Model
(CustomerId, OrderStatus.Shipped)
          |
          v
Anti-Corruption Layer
(Translates data, concepts and operations)
          |
          v
Legacy ERP System
(CUST_NO, ORD_STS, S, X)

The key idea is that your domain model speaks its own language, even when an external system speaks a different language.

3. Practical example in C# and ASP.NET Core

Suppose the ERP system returns this response:

{
  "CUST_NO": "C1001",
  "ORD_STS": "S",
  "TOTAL_AMT": 2500
}

Your application should not force its domain model to use CUST_NO, ORD_STS, or "S".

Instead, create a domain model, an external DTO, and a translator.

Step 1: Define your domain model

public enum OrderStatus
{
    Pending,
    Shipped,
    Cancelled
}

public sealed class Order
{
    public string CustomerId { get; }
    public OrderStatus Status { get; }
    public decimal TotalAmount { get; }

    public Order(
        string customerId,
        OrderStatus status,
        decimal totalAmount)
    {
        CustomerId = customerId;
        Status = status;
        TotalAmount = totalAmount;
    }
}

This represents your application's business concepts rather than the ERP's database structure.

Step 2: Define the external system's DTO

public sealed class ErpOrderDto
{
    public string CUST_NO { get; set; } = "";
    public string ORD_STS { get; set; } = "";
    public decimal TOTAL_AMT { get; set; }
}

This class represents the data format received from the ERP. It belongs in the integration layer, not in the domain model.

Step 3: Implement the Anti-Corruption Layer

public sealed class ErpOrderTranslator
{
    public Order Translate(ErpOrderDto dto)
    {
        return new Order(
            customerId: dto.CUST_NO,
            status: TranslateStatus(dto.ORD_STS),
            totalAmount: dto.TOTAL_AMT
        );
    }

    private static OrderStatus TranslateStatus(string status)
    {
        return status switch
        {
            "P" => OrderStatus.Pending,
            "S" => OrderStatus.Shipped,
            "X" => OrderStatus.Cancelled,

            _ => throw new InvalidOperationException(
                $"Unknown ERP order status: {status}")
        };
    }
}

The translator converts the ERP's status codes into meaningful domain values. In a production application, you should also validate incoming data and handle integration errors appropriately.

Step 4: Use the translator in an application service

public sealed class OrderApplicationService
{
    private readonly IErpOrderClient _erpClient;
    private readonly ErpOrderTranslator _translator;

    public OrderApplicationService(
        IErpOrderClient erpClient,
        ErpOrderTranslator translator)
    {
        _erpClient = erpClient;
        _translator = translator;
    }

    public async Task<Order> GetOrderAsync(
        string orderId)
    {
        ErpOrderDto dto =
            await _erpClient.GetOrderAsync(orderId);

        return _translator.Translate(dto);
    }
}

Here, IErpOrderClient represents an integration interface that retrieves data from the ERP. It would need to be defined and implemented separately.

The application service coordinates the operation, while the translator protects the domain model from the external format.

4. Where does the Anti-Corruption Layer belong in DDD?

A typical arrangement is:

Presentation Layer
(Controllers / API endpoints)
          |
          v
Application Layer
(Use cases / application services)
          |
          v
Integration / Infrastructure Layer
(ACL translator, external API client, DTO mapping)
          |
          v
External System
(ERP / legacy application / third-party API)

In a typical DDD architecture:

  • Domain layer: Contains your business entities, value objects, and domain rules.
  • Application layer: Coordinates use cases and calls the necessary interfaces.
  • Infrastructure/integration layer: Implements communication with external systems and translates their models.
  • ACL: Usually implemented in the integration or infrastructure layer, with its placement depending on the architecture.

The domain should not depend on the external system's DTOs or SDK-specific models.

5. When should you use an ACL?

Situation ACL needed?
Integrating with a legacy ERP with unfamiliar data structures Yes, usually
Connecting to a third-party API with different business concepts Often
Migrating from an old monolith to a new bounded context Often
Integrating with another team's independently designed domain Depends on the mismatch
Calling a simple service that already uses your domain's language Possibly unnecessary
Mapping a few straightforward fields with no meaningful semantic differences A full ACL may be overengineering

An ACL is particularly valuable when the external system has a different ubiquitous language — the terminology and concepts used within its own domain.

6. Anti-Corruption Layer vs. related DDD concepts

Concept Purpose
Anti-Corruption Layer Protects your domain from another system's model
Repository Provides an abstraction for accessing and persisting domain objects
DTO Carries data between boundaries
Adapter Makes one interface work with another interface
Application Service Coordinates a use case
Bounded Context Defines a boundary within which a particular domain model and language apply
Context Map Describes relationships and integration patterns between bounded contexts

An ACL may use adapters, DTOs, translators, and application services. However, those patterns are not automatically an ACL on their own. They form an ACL when they deliberately protect your domain model from an external model.

7. Key benefits

  • Domain independence: External data formats do not dictate your domain design.
  • Maintainability: Changes in the external system can often be isolated to the integration layer.
  • Clear business language: Your code uses meaningful concepts such as Shipped instead of "S".
  • Better testing: You can test domain behavior without connecting to the real external system.
  • Controlled integration: You can validate data, translate errors, and reject unsupported values at the boundary.

Remember that an ACL does not automatically solve network failures, retries, timeouts, or data synchronization. Those concerns require additional integration design.

8. The most important takeaway

Think of an Anti-Corruption Layer as a protective translator between two different domain models.

  • Without an ACL, your domain may gradually become shaped by an external system.
  • With an ACL, the external system can keep its own terminology and structure while your application retains its own domain model.

DDD rule of thumb: When integrating with another bounded context, translate its language at the boundary instead of allowing its model to leak into your domain.

Back to Index
Asynchronous Messaging Pattern :👈 👉:Identify-bounded-contexts-in-DDD