Anti-Corruption Layer
debt(d7/e7/b7/t5)
Closest to 'only careful code review or runtime testing' (d7). While phpstan and deptrac are listed as detection tools, the detection_hints note 'automated: no' — deptrac can enforce layer boundaries if configured, and phpstan can flag type mismatches, but detecting that external model concepts have leaked into domain logic (e.g., Stripe\Charge used directly in Order) requires careful architectural review. No tool automatically detects conceptual model corruption. Scored d7 rather than d5 because the tools catch symptoms at best, not the core violation.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix says 'create a translation layer' but when an ACL is missing or bypassed, external model concepts (like CUSTNO, ACCT_STATUS_CD) have already spread throughout the domain. Retrofitting an ACL requires identifying all leakage points, creating domain value objects/DTOs, and replacing every reference to the external model — a cross-cutting refactor. This isn't quite architectural rework (e9) since the domain model itself may be sound, but it spans many files and touches integration seams throughout the codebase.
Closest to 'strong gravitational pull' (b7). An ACL is a load-bearing architectural boundary that applies across web, CLI, and queue-worker contexts. Every future integration and every change to external system interactions must flow through it. The common_mistakes show that if it can be bypassed, it will be — meaning it shapes how every developer interacts with external systems. It doesn't quite define the system's entire shape (b9), but it strongly shapes all integration work and domain model purity.
Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The misconception field states clearly: developers think an ACL is just an adapter (interface translation), when it actually handles conceptual model differences — renaming and restructuring concepts, not just wrapping method signatures. This is a meaningful trap because adapters are a well-known pattern, and developers naturally reach for that simpler mental model. However, once explained, the distinction is graspable, and it doesn't contradict a similar concept from another domain (which would be t7).
Also Known As
TL;DR
Explanation
When integrating with a legacy system, third-party API, or a different bounded context, their models and terminology will differ from yours. Directly using their types pollutes your domain. An ACL sits at the boundary and translates: their 'custno' becomes your Customer, their 'amt_due' becomes your Money. It also protects against upstream changes — when their API changes, only the ACL needs updating. Evans described it in Domain-Driven Design as a way to keep a clean domain when working with messy legacy systems.
Diagram
flowchart LR
subgraph YourDomain
DOM[Clean Domain Model<br/>Customer, Money, Order]
APP[Application Services]
end
subgraph ACL
TRANS[Translator<br/>Legacy to Domain]
end
subgraph LegacySystem
LEG[CUST_NO, AMT_DUE_TTL<br/>ACCT_STATUS_CD]
end
LEG -->|raw legacy data| TRANS
TRANS -->|clean domain objects| APP
APP --- DOM
DOM -.->|never sees| LEG
style DOM fill:#6e40c9,color:#fff
style APP fill:#6e40c9,color:#fff
style TRANS fill:#238636,color:#fff
style LEG fill:#f85149,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- ACL that passes through the external model unchanged — it must translate to your domain's concepts.
- Not making the ACL a clear architectural boundary — if it can be bypassed, it will be.
- ACL that is too thick — it should translate, not contain business logic.
- One ACL for all integrations — each external system should have its own dedicated ACL.
Avoid When
- The external model is already compatible with your domain — an ACL adds translation overhead for no gain.
- The boundary is temporary and will be unified soon — build the unified model instead.
- The ACL itself becomes a translation dumping ground with no clear ownership.
When To Use
- Integrating a legacy system whose model would corrupt your clean domain if used directly.
- Consuming a third-party API whose concepts don't map cleanly to your domain.
- Protecting a bounded context from the modelling decisions of adjacent contexts.
- Any integration where you want to insulate your domain from external model changes.
Code Examples
// No ACL — legacy model leaks into domain:
class OrderService {
public function processOrder(array $legacyData): void {
$custNo = $legacyData['CUST_NO']; // Legacy naming leaks in
$amtDue = $legacyData['AMT_DUE_TTL_INCL_TAX']; // Incomprehensible
$stCd = $legacyData['ACCT_STATUS_CD']; // Magic codes everywhere
}
}
// ACL translates legacy model to domain model:
class LegacyOrderTranslator {
public function toDomain(array $legacy): Order {
return new Order(
customer: new CustomerId($legacy['CUST_NO']),
total: Money::fromCents((int)($legacy['AMT_DUE_TTL_INCL_TAX'] * 100)),
status: $this->translateStatus($legacy['ACCT_STATUS_CD'])
);
}
private function translateStatus(string $code): OrderStatus {
return match($code) {
'A' => OrderStatus::Active,
'S' => OrderStatus::Suspended,
default => throw new UnknownStatusCode($code)
};
}
}