← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Anti-Corruption Layer

architecture Advanced
debt(d7/e7/b7/t5)
d7 Detectability Operational debt — how invisible misuse is to your safety net

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.

e7 Effort Remediation debt — work required to fix once spotted

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.

b7 Burden Structural debt — long-term weight of choosing wrong

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.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

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).

About DEBT scoring →

Also Known As

ACL translation layer adapter layer

TL;DR

A translation layer between two systems with different models — preventing a legacy or external system's concepts and terminology from leaking into the domain model.

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

An ACL is just an adapter — adapters handle interface differences; ACLs handle conceptual model differences, renaming and restructuring concepts, not just wrapping method signatures.

Why It Matters

Without an ACL, legacy system concepts like 'CUSTNO', 'ACCT_STATUS_CD', and 'AMT_DUE_TTL' spread through your clean domain model, making it incomprehensible and tightly coupled to the legacy schema.

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

✗ Vulnerable
// 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
    }
}
✓ Fixed
// 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)
        };
    }
}

Added 15 Mar 2026
Edited 25 Mar 2026
Views 47
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings F 0 pings S 2 pings S 1 ping M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 1 ping S 1 ping M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 3 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S
No pings yet today
Google 18 Perplexity 9 Amazonbot 7 ChatGPT 3 Unknown AI 2 Ahrefs 1
crawler 39 crawler_json 1
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: High
⚡ Quick Fix
Create a translation layer between your domain model and a third-party API or legacy system — map their concepts to yours, never let their model leak into your domain
📦 Applies To
any web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Third-party API objects used directly in domain logic; Stripe\Charge used in Order domain class instead of domain PaymentResult
Auto-detectable: ✗ No phpstan deptrac
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: File Tests: Update

✓ schema.org compliant