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…
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:
┌─────────────────────────────────────────┐│ 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.
// ACL translation — Stripe internal types only past this moduletype BankRecord = { raw: string }; // never leaves ACL packageinterface 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 pendingstatus: 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.
Production code example
ACL service skeleton with audit archive, DLQ, and idempotent event emission:
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.
1AdvancedQuestionHow is an Anti-Corruption Layer different from a generic adapter at Stripe?+
Answer
Follow-up
2AdvancedQuestionPartner sends a file format change with 48 hours notice. Walk through your response.+
Answer
Follow-up
3AdvancedQuestionEngineers argue ACL duplicates logic that exists in the partner's API docs. How do you decide what belongs in ACL vs core?+
Answer
Follow-up
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.