Skip to content

// Domain-Driven Design · 04

Anti-Corruption Layer

Foreign modelleads toTranslatorleads toClean domain model

Translate a foreign model at the border so it never leaks inside.

How it fits together

Anti-Corruption Layer structureOrders domainknows Customer, Statusnever sees ERP typesErpCustomerTranslator1 → Active, 7 → Closedunknown code → reject«interface»CustomerDirectoryFind(id): CustomerErpCustomerDirectoryadapter + facade«external»Legacy ERPSOAP GetCustomerCUST_STAT_CD = 71 Find(42)2 SOAP call3 translate
uses / callsimplements· numbered steps replay in order
  • Orders domain uses / calls CustomerDirectory (1 Find(42))
  • ErpCustomerDirectory implements CustomerDirectory
  • ErpCustomerDirectory uses / calls Legacy ERP (2 SOAP call)
  • ErpCustomerDirectory uses / calls ErpCustomerTranslator (3 translate)

Spot it

  • Vendor field names appear in domain classes
  • CUST_STAT_CD == 7 checks scattered in code
  • A vendor upgrade breaks unrelated features

It costs

Extra mapping code and tests to maintain, plus a layer that can hide slow remote calls.

In code

public enum CustomerStatus { Active, OnHold, Closed }​public sealed record ErpCustomerDto(string CUST_NO, string CUSTNM, int CUST_STAT_CD);​public sealed class ErpCustomerTranslator{    public Customer ToDomain(ErpCustomerDto erp) => new(        new CustomerId(erp.CUST_NO.Trim()),        erp.CUSTNM.Trim(),        erp.CUST_STAT_CD switch        {            1 => CustomerStatus.Active,            2 => CustomerStatus.OnHold,            7 => CustomerStatus.Closed,            var code => throw new UnknownErpCodeException(nameof(erp.CUST_STAT_CD), code),        });}​public sealed class ErpCustomerDirectory(IErpSoapClient erp, ErpCustomerTranslator translator)    : ICustomerDirectory{    public async Task<Customer?> FindAsync(CustomerId id, CancellationToken ct)    {        var dto = await erp.GetCustomerAsync(id.Value, ct);        return dto is null ? null : translator.ToDomain(dto);    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

When your context must talk to a system whose model you do not control, conforming to it spreads its names, codes and quirks through your code. An anti-corruption layer sits on the downstream side of the boundary: it speaks the foreign model on the outside and yours on the inside.

Inside it you usually find a facade that simplifies the remote API, an adapter that implements a port your domain defines, and a translator that converts data in both directions. Unknown values should fail at the boundary instead of defaulting silently to something plausible.

It pairs well with the strangler fig: new code reaches the legacy system only through the layer, so the legacy can be replaced behind it. When NOT to: if the upstream model is good and stable, such as a published language you are happy with, conforming is cheaper.

Ask for it

  • anti-corruption layer
  • translator
  • adapter
  • facade
  • port
  • conformist (the alternative)

Add an anti-corruption layer between Orders and the legacy ERP: a CustomerDirectory port in the domain returning our Customer, an adapter calling the ERP SOAP API, and a translator mapping CUST_STAT_CD 1/2/7 to Active/OnHold/Closed that rejects unknown codes. No ERP types outside the adapter.