Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 55

    Anti Corruption Layer

    Anti-Corruption Layer (ACL) is a translating boundary that isolates your domain model from legacy or external systems whose vocabulary, invariants, and failure modes would corru…

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Anti-Corruption Layer (ACL) is a translating boundary that isolates your domain model from legacy or external systems whose vocabulary, invariants, and failure modes would corrupt your design. At Stripe, ACLs sit between modern API services and partner banks, legacy settlement systems, and acquirer protocols — translating without letting their semantics leak inward.

    Real production story

    Stripe's enterprise billing team integrated with a European bank's 1990s settlement feed: fixed-width files, ambiguous date formats, and status codes that meant "pending" in the bank's docs but "settled" in production. Junior engineers mapped fields directly into Stripe's Charge and BalanceTransaction models. Within two weeks, reconciliation reported €2.3M in phantom settled funds — finance halted EU rollout.

    The staff architect introduced an ACL service: BankSettlementAdapter consumed raw feed, translated to internal SettlementEvent value objects, and exposed only clean events to billing core. Legacy semantics stayed in the adapter — including the "pending that means settled" quirk, documented in ADR-0892. Billing core never imported bank field names. Reconciliation accuracy restored to 99.999% within one sprint.

    Business problem

    Business pressure: Stripe must onboard enterprise merchants weekly while integrating with heterogeneous banking systems whose models predate REST by decades. Direct model mapping spreads legacy corruption into core domain code.

    • Revenue at risk: Reconciliation errors block enterprise deals — CFOs audit settlement accuracy before signing $1M+ contracts.
    • Engineering velocity: Every bank integration mapped directly into core forces billing team to understand COBOL semantics — unsustainable at 40+ integrations.
    • Compliance / trust: SOC2 and PCI require clear boundary ownership — "it's in billing core" fails when legacy quirks cause double charges.

    Architecture overview

    Anti-Corruption Layer in production is a dedicated module or service that: (1) receives external representation, (2) validates and normalizes, (3) translates to internal domain events/entities, (4) never exposes external types past the boundary. DDD bounded context pattern — ACL is the boundary's outward face.

    • Definition: Translating facade that prevents foreign domain models from corrupting your core model.
    • When to adopt: Integrating legacy, partner, or third-party systems with incompatible semantics or frequent external changes.
    • When to defer: Partner API is well-designed, stable, and aligns with your model (rare at enterprise scale).
    • Operability: ACL logs raw + translated payloads for audit; versioned translation rules per partner.

    Architecture motivation

    Why architects care: ACL protects domain integrity when integrating upstream/downstream systems you don't control. The naive alternative — direct DTO mapping into domain entities — couples your model to theirs and makes every partner change a core refactor.

    • Force: External system model contradicts your ubiquitous language (e.g., "pending" ≠ pending).
    • Constraint: Cannot rewrite partner systems; must integrate on their timeline and format.
    • Outcome: Translation isolated in ACL; core domain uses only internal types; partner changes touch ACL only.

    Internal architecture

    Stripe billing ACL topology — core never sees bank field names:

    text
    ┌─────────────────────────────────────────┐
    │ Billing Core (domain) │
    │ PaymentIntent · Invoice · LedgerEntry │
    └────────────────────┬────────────────────┘
    │ SettlementEvent (internal)
    ┌─────────────────────────────────────────┐
    │ Anti-Corruption Layer (adapter) │
    │ BankSettlementAdapter │
    │ - parse fixed-width │
    │ - map status codes (incl. quirks) │
    │ - emit SettlementEvent │
    └────────────────────┬────────────────────┘
    │ raw bytes / SFTP
    ┌─────────────────────────────────────────┐
    │ External: Bank mainframe feed │
    │ COBOL copybook · ambiguous dates │
    └─────────────────────────────────────────┘

    Data flow

    Primary path: SFTP poll retrieves settlement file → ACL parses records → each record validated → translated to SettlementEvent → published to Kafka → billing core consumer updates ledger idempotently by externalReferenceId.

    • Write path: ACL is only writer of raw feed to cold storage (S3) for audit; translated events are immutable.
    • Read path: Billing core queries ledger — never queries bank format.
    • Async path: Failed translations go to DLQ with raw payload; ops replay after rule fix.
    typescript
    // ACL translation — Stripe internal types only past this module
    type BankRecord = { raw: string }; // never leaves ACL package
    interface SettlementEvent {
    externalReferenceId: string;
    amountCents: number;
    currency: string;
    status: "pending" | "settled" | "failed";
    settledAt: Date | null;
    }
    class BankSettlementAdapter {
    translate(record: BankRecord): SettlementEvent {
    const parsed = this.parseFixedWidth(record.raw);
    return {
    externalReferenceId: parsed.ref,
    amountCents: parseInt(parsed.amount, 10),
    currency: parsed.ccy,
    // ADR-0892: code "03" means settled in prod despite docs saying pending
    status: parsed.statusCode === "03" ? "settled" : this.mapStatus(parsed.statusCode),
    settledAt: parsed.statusCode === "03" ? this.parseDate(parsed.date) : null,
    };
    }
    }

    System design diagram

    Two diagrams show the Anti Corruption Layer topology and the primary request/event path used in production at scale.

    Anti Corruption Layer — system view
    Stripe core
    Edge
    ACL service
    Core
    Translation
    Data
    Legacy bank
    Async
    High-level topology for Anti Corruption Layer.
    Anti Corruption Layer — request / event flow
    Raw feed
    Ingress
    Normalize
    Store
    Translate
    Store
    Domain event
    Emit
    Follow this path when reviewing production designs.

    Production code example

    ACL service skeleton with audit archive, DLQ, and idempotent event emission:

    typescript
    async function processSettlementFile(partnerId: string, file: Buffer): Promise<void> {
    const archiveKey = await s3.put(`raw/${partnerId}/${hash(file)}`, file);
    const adapter = AdapterRegistry.get(partnerId);
    for (const record of adapter.parse(file)) {
    try {
    const event = adapter.translate(record);
    await kafka.publish("settlement.events", event, {
    key: event.externalReferenceId,
    headers: { partnerId, archiveKey },
    });
    } catch (err) {
    await dlq.push({ partnerId, archiveKey, record, err: String(err) });
    metrics.increment("acl.translation.failure", { partnerId });
    }
    }
    }

    Enterprise case study

    Stripe EU bank integration: ACL team owns all partner adapters; billing core team rejects PRs that import external types. Golden file tests per partner with recorded production samples (redacted).

    • Before: Direct mapping; €2.3M phantom settled funds; EU rollout halted 6 weeks.
    • Decision: Mandatory ACL for all bank feeds; ADR per partner quirk; idempotent core consumers.
    • After: 99.999% reconciliation accuracy; 12 new bank integrations in 18 months without core model changes.

    Trade-offs

    • ACL maintenance vs core purity: ACL grows with partner quirks — cheaper than corrupting core domain.
    • Latency vs isolation: Extra translation hop adds milliseconds — acceptable for batch settlement; use sync ACL carefully on hot path.
    • Duplication vs coupling: ACL duplicates some partner logic — intentional; DRY across bounded contexts is an anti-pattern.

    Security considerations

    Security is architectural: ACL handles raw financial data from untrusted formats — validate aggressively; never eval or dynamic-parse unvalidated input.

    • Identity: SFTP credentials scoped per partner; ACL service account cannot write to billing core DB directly — events only.
    • Data: Raw feed encrypted at rest; PII stripped before event emission where possible.
    • Supply chain: ACL deploy independent of core — partner hotfix does not require billing core release.

    Scalability analysis

    Scale dimensions: Stripe processes billions in settlement events monthly — ACL must shard by partner and process files in parallel without blocking core consumers.

    • Horizontal scale: ACL workers autoscale per partner queue depth; core consumers scale on Kafka partition count.
    • Hot spots: Large enterprise merchant generates 10× file volume — dedicated partner shard prevents head-of-line blocking.
    • Cost: Raw feed archival in S3 is cheap insurance vs reconciliation audit failures.

    Failure scenarios

    What breaks: Partner changes status code without notice; ACL translation bug emits wrong settledAt; core consumer double-applies event without idempotency key.

    • Silent partner change: DLQ rate spike alerts; raw archive enables replay after hotfix.
    • Translation bug: Canary partner on new ACL version before fleet rollout.
    • Double apply: Core must idempotize on externalReferenceId — ACL does not guarantee exactly-once delivery.

    Staff engineer insights

    • Name partner quirks in ADRs inside the ACL repo — "pending means settled" is not obvious six months later.
    • Golden file tests with real (redacted) production samples beat synthetic unit test data for ACL correctness.
    • If core engineers import external DTOs, the ACL has failed — enforce with module boundary lint rules.

    Interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionHow is an Anti-Corruption Layer different from a generic adapter at Stripe?+

    Answer

    Adapter is structural (interface conversion). ACL is strategic — it protects domain integrity by refusing to let external semantics past the boundary. ACL owns translation rules, documents partner quirks in ADRs, and exposes only internal domain events. Generic adapter might pass through partner DTOs — ACL never does.

    Follow-up

    Where would you place ACL in a hexagonal architecture diagram?
    2AdvancedQuestionPartner sends a file format change with 48 hours notice. Walk through your response.+

    Answer

    Pull archived samples; update ACL parser with versioned rules; add golden file test; deploy ACL only (not core); replay DLQ from archive if needed; confirm reconciliation parity on shadow ledger before enabling. Core unchanged if SettlementEvent schema stable.

    Follow-up

    When does SettlementEvent schema itself need to evolve?
    3AdvancedQuestionEngineers argue ACL duplicates logic that exists in the partner's API docs. How do you decide what belongs in ACL vs core?+

    Answer

    If the logic exists because the partner's model differs from yours — ACL. If it's business rule for all settlements regardless of partner — core. Rule of thumb: would this rule exist if we dropped this partner tomorrow? No → ACL. Yes → core.

    Follow-up

    Give an example of a rule that seems like ACL but belongs in core.

    Architecture review questions

    • Are quality attributes (latency, availability, consistency) explicit with SLOs for Anti-Corruption Layer?
    • Is the failure/degraded mode documented — including what happens when dependencies are down?
    • Are boundaries and ownership clear on an architecture diagram a new engineer understands in 10 minutes?
    • Is there an ADR capturing alternatives considered and why they were rejected?
    • Can this design scale 10× on traffic and 3× on engineering headcount without a rewrite?
    • Security: authn/authz, encryption, and blast radius reviewed at every external interface?

    Summary

    Anti-Corruption Layer at enterprise scale is a deliberate translating boundary that keeps foreign semantics out of your core domain. At Stripe, ACLs enable rapid partner onboarding without billing model pollution — wire audit archives, DLQ replay, and module boundary enforcement.

    Ready to mark this lesson complete?Track your journey across the entire course.