Anti-Corruption Layer: Bridge Legacy & New Systems

intermediate 39 min read Updated 2026-02-11

TL;DR

An Anti-Corruption Layer (ACL) is a translation boundary that sits between your domain model and external systems with different semantics, preventing legacy or third-party models from polluting your clean architecture. It acts as a bidirectional adapter that translates requests and responses, allowing your system to maintain its own ubiquitous language while integrating with systems that speak differently. Think of it as a diplomatic interpreter that ensures your internal team never has to speak the messy language of external partners.

Cheat Sheet: ACL = Facade + Adapter between bounded contexts | Protects domain purity | Translates models bidirectionally | Essential for legacy integration and microservices boundaries.

The Analogy

Imagine you’re building a modern smart home system with clean, intuitive APIs, but you need to integrate with a 1990s security system that uses cryptic numeric codes and bizarre command sequences. Instead of forcing your elegant home automation logic to understand “ARM_MODE_3_ZONE_BYPASS_12” everywhere, you build a translator module. This translator speaks the old system’s weird language on one side and your clean “setSecurityMode(‘away’)” API on the other. Your core system never knows about the legacy mess—it just calls clean methods. That translator is your Anti-Corruption Layer. It keeps the old system’s corruption from spreading into your beautiful new architecture, while still allowing the two systems to work together seamlessly.

Why This Matters in Interviews

The Anti-Corruption Layer comes up in interviews about microservices migrations, legacy system modernization, and domain-driven design. Interviewers use it to assess whether you understand architectural boundaries and can prevent technical debt from spreading. Strong candidates explain ACL as a strategic pattern for managing complexity at integration points, not just a simple adapter. They discuss the tradeoffs—added latency and maintenance overhead versus architectural purity—and know when to use it versus when direct integration is acceptable. Senior candidates connect ACL to bounded contexts, explain how it enables parallel team development, and discuss implementation strategies like whether the ACL should be owned by the upstream or downstream team.


Core Concept

The Anti-Corruption Layer is a defensive design pattern that creates an isolation boundary between your system’s domain model and external systems with incompatible semantics. First described by Eric Evans in Domain-Driven Design, it prevents the models, terminology, and design decisions of external systems from leaking into and corrupting your carefully crafted domain. The ACL acts as a bidirectional translator, converting external concepts into your domain’s ubiquitous language on the way in, and translating your domain operations into external system calls on the way out.

This pattern becomes critical when integrating with legacy systems, third-party APIs, or other bounded contexts that you don’t control. Without an ACL, you’re forced to make compromises in your domain model to accommodate external constraints—using their field names, their validation rules, their data structures. Over time, these compromises accumulate, and your once-clean domain becomes a patchwork of different mental models. The ACL prevents this corruption by containing all translation logic in a dedicated layer, keeping your domain pure and focused on business problems rather than integration concerns.

The ACL is not just a technical adapter—it’s a strategic architectural decision that trades some implementation complexity (maintaining the translation layer) for long-term maintainability and team velocity. By isolating external dependencies, you enable your team to evolve the domain model independently, replace external systems without touching core logic, and maintain a consistent vocabulary across your codebase. Companies like Stripe and Shopify use ACLs extensively to integrate with hundreds of payment processors and shipping providers without letting external APIs dictate their internal architecture.

How It Works

Step 1: Define Your Domain Model Start by designing your domain model based purely on business needs, using your team’s ubiquitous language. For example, if you’re building an e-commerce system, you might have a clean Order entity with fields like customerId, items, totalAmount, and status. This model reflects how your business thinks about orders, not how any external system structures data. The key is to make no compromises for external systems at this stage—your domain should be pure.

Step 2: Identify the External System’s Model Document the external system’s data structures, APIs, and semantics. A legacy ERP system might represent orders as ORDRHDR records with cryptic fields like CUST_NO, ORD_STS_CD, and TOT_AMT_CENTS. The external model likely uses different terminology, different data types, different validation rules, and different business logic. Understanding these differences is crucial because the ACL’s job is to bridge this semantic gap.

Step 3: Build the Translation Layer Implement the ACL as a set of adapters, facades, and translators that sit between your domain and the external system. When your domain needs to fetch an order, it calls orderRepository.findById(orderId) using your clean interface. The ACL intercepts this call, translates it to the external system’s query format (perhaps a SOAP request with specific XML structure), makes the external call, receives the response, and translates the ORDRHDR record back into your domain’s Order object. The translation includes mapping field names, converting data types (cents to dollars), translating status codes, and applying any business rule transformations.

Step 4: Handle Bidirectional Translation The ACL must work in both directions. When your domain creates or updates an order, the ACL translates your domain events into the external system’s commands. If your domain emits an OrderPlaced event with a clean structure, the ACL converts this into whatever format the legacy ERP expects—perhaps a batch file upload or a specific API call sequence. This bidirectional translation ensures your domain never directly interacts with external semantics.

Step 5: Maintain Semantic Consistency The ACL enforces your domain’s invariants even when the external system has different rules. If your domain requires email addresses but the legacy system allows null emails, the ACL handles this mismatch—perhaps by using a default value, throwing a domain exception, or enriching data from another source. The ACL is responsible for ensuring that data crossing the boundary always conforms to your domain’s expectations, protecting your model’s integrity.

Step 6: Isolate and Test Implement the ACL as a separate module with clear interfaces, making it easy to test in isolation and swap implementations. You should be able to replace the real external system with a mock during testing, or switch from one payment processor to another by only changing the ACL implementation, leaving your domain untouched. This isolation is the pattern’s primary value—your domain remains stable while external dependencies change.

Bidirectional Translation Flow Through ACL

graph LR
    subgraph Domain Layer
        DS["Domain Service<br/><i>Order Management</i>"]
        DM["Domain Model<br/><i>Order Entity</i>"]
    end
    
    subgraph Anti-Corruption Layer
        Facade["ACL Facade<br/><i>Translation Logic</i>"]
        Adapter["External Adapter<br/><i>Protocol Handler</i>"]
    end
    
    subgraph External System
        API["Legacy ERP API<br/><i>SOAP/XML</i>"]
        DB[("Legacy DB<br/><i>ORDRHDR Table</i>")]
    end
    
    DS --"1. createOrder(Order)"--> Facade
    Facade --"2. Translate to<br/>ERP format"--> Adapter
    Adapter --"3. SOAP Request<br/>(ORDRHDR XML)"--> API
    API --"4. Insert"--> DB
    DB --"5. Response"--> API
    API --"6. SOAP Response"--> Adapter
    Adapter --"7. Parse XML"--> Facade
    Facade --"8. Translate to<br/>Domain Model"--> DS
    DS --"9. Return Order"--> DM

The ACL intercepts domain operations, translates them into external system format, executes the external call, and translates responses back into domain objects. The domain never directly interacts with external types or protocols.

Key Principles

Principle 1: Translation, Not Leakage The ACL must fully translate external concepts into domain concepts, never allowing external types or terminology to leak into your domain. If an external API returns a PaymentProcessorResponse object, your domain should never see it—the ACL translates it into a domain PaymentResult. This means no external DTOs in domain method signatures, no external enums in domain logic, and no external validation rules in domain entities. Stripe’s internal architecture exemplifies this: their domain models use concepts like “charge” and “payment intent,” but they integrate with dozens of payment processors, each with different terminology. The ACL for each processor translates processor-specific concepts into Stripe’s domain language, ensuring the core payment engine never knows whether it’s talking to Visa, PayPal, or a bank transfer system.

Principle 2: Semantic Preservation The ACL must preserve your domain’s semantics and invariants, even when the external system has different rules or capabilities. If your domain requires that orders have a valid shipping address, but the external system allows orders without addresses, the ACL must enforce this constraint at the boundary—either by rejecting invalid data, enriching it from another source, or applying domain-specific defaults. This principle prevents the external system’s looser constraints from corrupting your domain’s integrity. Amazon’s order management system maintains strict invariants about order states and transitions, but integrates with thousands of seller systems with varying data quality. Their ACL validates and enriches seller data to meet Amazon’s domain requirements before it enters the core order processing pipeline.

Principle 3: Bidirectional Independence The ACL enables both systems to evolve independently by decoupling their lifecycles. Your domain can add new fields, change validation rules, or refactor entities without coordinating with the external system, as long as the ACL can still perform the translation. Similarly, if the external system changes its API version or data format, you only update the ACL, leaving your domain untouched. This independence is crucial for organizational agility. When Shopify migrated from a monolithic architecture to microservices, they used ACLs at service boundaries to allow teams to refactor their domains independently. A team could completely redesign their inventory model without breaking integrations, because the ACL maintained the old contract while translating to the new internal structure.

Principle 4: Explicit Ownership The ACL should have a clear owner—typically the team that owns the domain being protected. This team is responsible for defining the translation logic, maintaining the ACL implementation, and ensuring it correctly represents their domain’s semantics. The external system’s team should not dictate how the ACL works; they only need to provide a stable interface. This ownership clarity prevents the ACL from becoming a dumping ground for integration logic that nobody maintains. At Netflix, when microservices integrate with legacy systems, the downstream service team owns the ACL. They decide how to translate the legacy system’s data into their domain model, ensuring the translation serves their needs rather than being a lowest-common-denominator compromise.

Principle 5: Bounded Context Boundary In Domain-Driven Design terms, the ACL marks the boundary between bounded contexts. Each bounded context has its own ubiquitous language and model, and the ACL is where these languages meet and translate. This principle helps you identify where ACLs are needed: at every integration point between contexts with different models. Not every service boundary needs an ACL—if two services share the same domain model and language (perhaps because they’re in the same bounded context), a simple shared library or direct API calls may suffice. But when contexts diverge, the ACL prevents one context’s model from corrupting another. Uber’s marketplace platform uses ACLs between the rider-facing context (which thinks in terms of trips and destinations) and the driver-facing context (which thinks in terms of jobs and routes), even though they’re describing the same underlying reality.

Semantic Translation vs. Simple Wrapping

graph TB
    subgraph Wrong Approach: Leaky Abstraction
        D1["Domain Code"]
        W["Thin Wrapper"]
        E1["External API"]
        D1 --"Uses external types<br/>PaymentProcessorResponse"--> W
        W --"Direct pass-through"--> E1
        E1 --"External error codes<br/>PROC_ERR_3421"--> W
        W --"Leaks to domain"--> D1
    end
    
    subgraph Right Approach: True ACL
        D2["Domain Code"]
        ACL["Anti-Corruption Layer"]
        E2["External API"]
        D2 --"Uses domain types<br/>PaymentResult"--> ACL
        ACL --"Translates semantics<br/>Maps fields & rules"--> E2
        E2 --"External error codes<br/>PROC_ERR_3421"--> ACL
        ACL --"Domain exceptions<br/>PaymentDeclined"--> D2
    end

A thin wrapper exposes external concepts to the domain, causing corruption. A true ACL translates all semantics—data structures, terminology, error codes, and business rules—keeping the domain pure.


Deep Dive

Types / Variants

Variant 1: Facade-Based ACL The facade pattern provides a simplified interface to a complex external system, hiding its complexity behind a clean API that matches your domain’s needs. This variant is ideal when the external system has a baroque API with many methods, complex call sequences, or intricate configuration. Your domain calls simple methods like orderService.placeOrder(order), and the facade handles the complexity of calling the external system’s 15-step order creation workflow. The facade may also aggregate multiple external calls into a single domain operation, reducing coupling. When to use: Integrating with complex legacy systems or enterprise software with unwieldy APIs. Pros: Simplifies domain code dramatically, centralizes external system knowledge, makes testing easier. Cons: Can hide too much complexity, making debugging harder; may need to expose some underlying complexity for advanced use cases. Example: Shopify’s ACL for enterprise ERP systems like SAP provides a simple syncInventory() method that hides the complexity of SAP’s multi-step inventory update protocol, including authentication, session management, transaction handling, and error recovery.

Variant 2: Adapter-Based ACL The adapter pattern converts one interface into another, focusing on structural translation rather than complexity hiding. This variant is common when the external system’s API is reasonably clean but uses different data structures, naming conventions, or protocols than your domain. The adapter translates between your domain’s Order object and the external system’s PurchaseOrder DTO, mapping fields, converting types, and handling format differences. When to use: Integrating with third-party APIs that are well-designed but semantically different from your domain. Pros: Lightweight, focused on translation, easy to understand and maintain. Cons: May not provide enough abstraction if the external system is complex; can become bloated with mapping logic. Example: Stripe’s ACL for payment processors uses adapters to translate between Stripe’s internal payment model and each processor’s specific data format. The Visa adapter translates Stripe’s PaymentIntent into Visa’s authorization request format, while the PayPal adapter translates the same PaymentIntent into PayPal’s completely different API structure.

Variant 3: Service-Based ACL This variant implements the ACL as a separate microservice that sits between your domain service and the external system. All communication flows through this intermediary service, which handles translation, protocol conversion, and often adds capabilities like caching, rate limiting, and monitoring. When to use: When multiple services need to integrate with the same external system, or when the external system is particularly unreliable or slow. Pros: Centralized integration logic, shared across multiple consumers; can add cross-cutting concerns like caching and circuit breakers; enables independent scaling and deployment. Cons: Adds network latency and operational complexity; becomes a single point of failure if not designed for high availability; requires careful API design to serve multiple consumers. Example: Netflix’s integration with legacy billing systems uses a dedicated ACL service that multiple downstream services call. This service translates Netflix’s modern subscription model into the legacy system’s account structure, caches frequently accessed data to reduce load on the legacy system, and provides a circuit breaker to protect Netflix’s services when the legacy system is slow or down.

Variant 4: Event-Driven ACL Instead of synchronous translation, this variant uses events and message queues to decouple your domain from the external system. Your domain publishes domain events (like OrderPlaced), and the ACL subscribes to these events, translates them, and pushes them to the external system asynchronously. Similarly, the ACL listens for external system events, translates them, and publishes domain events that your system consumes. When to use: When eventual consistency is acceptable, when the external system is slow or unreliable, or when you need to decouple deployment and scaling. Pros: Excellent decoupling, natural fit for event-driven architectures, handles external system downtime gracefully through retry mechanisms. Cons: Eventual consistency can complicate business logic; requires robust event infrastructure; debugging distributed flows is harder. Example: Uber’s driver payment system uses an event-driven ACL to integrate with banking systems. When a driver completes a trip, Uber’s domain publishes a TripCompleted event. The ACL translates this into a payment instruction for the banking system, handling retries and idempotency. The banking system eventually confirms the payment, and the ACL translates this confirmation back into a domain event that updates the driver’s balance.

Variant 5: Repository-Based ACL This variant implements the ACL as part of the repository pattern, where the repository interface is defined by your domain, but the implementation handles translation to the external system’s data store. Your domain calls orderRepository.save(order), and the repository implementation translates the domain entity into whatever format the external system requires—perhaps SQL for a legacy database, or API calls for a SaaS system. When to use: When the external system is primarily a data store rather than a complex service, or when you want to hide data access details from your domain. Pros: Natural fit for data-centric integrations, keeps domain entities clean, leverages familiar repository pattern. Cons: May not handle complex business logic in the external system well; can blur the line between data access and integration logic. Example: Airbnb’s listing service uses repository-based ACLs to integrate with various property management systems (PMS). The domain defines a ListingRepository interface with methods like findAvailableDates(). Each PMS has its own repository implementation that translates these calls into PMS-specific API requests, handling different availability models, date formats, and booking rules.

ACL Implementation Variants

graph TB
    subgraph Facade-Based ACL
        F1["Simple Interface<br/>placeOrder()"]
        F2["Hides Complexity<br/>15-step workflow"]
        F3["Legacy ERP<br/>Complex API"]
        F1 --> F2 --> F3
    end
    
    subgraph Adapter-Based ACL
        A1["Domain Order"]
        A2["Field Mapping<br/>customerId → CUST_NO"]
        A3["External DTO<br/>PurchaseOrder"]
        A1 --> A2 --> A3
    end
    
    subgraph Service-Based ACL
        S1["Service A"]
        S2["Service B"]
        S3["ACL Microservice<br/>+ Cache + Circuit Breaker"]
        S4["External System"]
        S1 & S2 --> S3 --> S4
    end
    
    subgraph Event-Driven ACL
        E1["Domain Service"]
        E2["Event Bus"]
        E3["ACL Consumer<br/>Async Translation"]
        E4["External System"]
        E1 --"Publish<br/>OrderPlaced"--> E2
        E2 --"Subscribe"--> E3
        E3 --"Translate & Push"--> E4
    end

Different ACL variants serve different needs: Facade simplifies complex APIs, Adapter handles structural translation, Service-based centralizes integration, and Event-driven enables async decoupling.

Trade-offs

Tradeoff 1: Architectural Purity vs. Performance The ACL adds an extra layer of translation, which introduces latency and computational overhead. Every request must be translated twice—once on the way to the external system and once on the way back. For high-throughput systems, this overhead can be significant. Option A: Strict ACL with full translation maintains perfect domain isolation but may add 10-50ms per request depending on translation complexity. Option B: Leaky abstraction with direct integration eliminates translation overhead but allows external concepts to pollute your domain. Decision framework: Use strict ACL for core domain logic that changes frequently or when long-term maintainability is critical. Accept some leakage for high-performance paths that are stable and unlikely to change. Stripe uses strict ACLs for their core payment processing logic (which must remain pure and testable), but allows some direct integration in their analytics pipeline where performance matters more than domain purity. They accept that analytics code is more coupled to external systems because it’s less critical and changes less frequently.

Tradeoff 2: Translation Complexity vs. Model Divergence As your domain model and the external system’s model diverge over time, the ACL’s translation logic becomes more complex. You might need to aggregate multiple external calls to construct a single domain entity, or perform complex transformations to map between fundamentally different concepts. Option A: Complex ACL with sophisticated translation keeps your domain pure but the ACL becomes a maintenance burden with intricate mapping logic, error handling, and edge cases. Option B: Compromise domain model to more closely match the external system, simplifying the ACL but corrupting your domain with external concerns. Decision framework: Invest in complex translation when the domain model is central to your competitive advantage and will evolve independently. Compromise the domain model when the external system is stable, unlikely to be replaced, and the domain is not a core differentiator. Amazon’s retail domain maintains complex ACLs for seller integrations because their internal order model is a core asset that must evolve independently. But their internal tools for warehouse management use simpler ACLs and accept some model compromise because those domains are less critical and the external warehouse management systems are stable.

Tradeoff 3: Ownership and Team Boundaries Who owns the ACL—the team that owns the domain being protected, or the team that owns the external system? Option A: Downstream team owns ACL means the domain team controls how external data is translated into their model, ensuring it serves their needs. But they must understand the external system’s API and handle its quirks. Option B: Upstream team owns ACL means the external system’s team provides a domain-friendly interface, reducing the downstream team’s burden. But the upstream team may not understand the downstream domain well enough to provide the right abstractions. Decision framework: Downstream ownership is better when the domain is complex and the team needs full control over how external data is interpreted. Upstream ownership works when the external system is a shared platform serving many consumers and can provide a generic, domain-agnostic interface. At Netflix, microservice teams own their ACLs for integrating with legacy systems because each team’s domain is unique and they need control over translation. But shared platform services like authentication provide their own ACLs (in the form of client libraries) because they serve many consumers with similar needs.

Tradeoff 4: Synchronous vs. Asynchronous Translation Should the ACL translate requests synchronously (blocking until the external system responds) or asynchronously (using events and eventual consistency)? Option A: Synchronous ACL provides immediate feedback and simpler error handling, but couples your system’s availability to the external system’s availability. If the external system is down, your operations fail immediately. Option B: Asynchronous ACL decouples availability and provides better resilience, but introduces eventual consistency and complicates error handling (what if the external system rejects the request hours later?). Decision framework: Use synchronous ACL for operations that require immediate confirmation or when the external system is highly reliable. Use asynchronous ACL for operations where eventual consistency is acceptable or when the external system is unreliable. Uber uses synchronous ACLs for real-time operations like requesting a ride (riders need immediate confirmation) but asynchronous ACLs for driver payouts (eventual consistency is acceptable and banking systems can be slow).

Tradeoff 5: Shared ACL vs. Per-Consumer ACL If multiple services need to integrate with the same external system, should they share a single ACL service, or should each implement its own ACL? Option A: Shared ACL service centralizes integration logic, reduces duplication, and provides a single place to add capabilities like caching and rate limiting. But it becomes a coordination point that multiple teams depend on, and it must serve the lowest common denominator of all consumers’ needs. Option B: Per-consumer ACL gives each team full control and allows them to optimize for their specific use case, but leads to duplication and inconsistent integration patterns. Decision framework: Use a shared ACL when the external system is complex, expensive to integrate with, or has strict rate limits that require coordination. Use per-consumer ACLs when each consumer has significantly different needs or when team autonomy is more important than consistency. Shopify uses a shared ACL service for integrating with Stripe because all consumers need similar payment capabilities and Stripe has rate limits that require coordination. But they use per-consumer ACLs for integrating with various shipping providers because each consumer (retail, wholesale, dropshipping) has very different shipping needs.

ACL Ownership Models

graph LR
    subgraph Downstream Ownership
        D1["Domain Team<br/><i>Owns ACL</i>"]
        ACL1["ACL<br/><i>Custom Translation</i>"]
        U1["External System<br/><i>Generic API</i>"]
        D1 --"Full control<br/>over translation"--> ACL1
        ACL1 --"Must understand<br/>external API"--> U1
    end
    
    subgraph Upstream Ownership
        U2["External System Team<br/><i>Owns ACL</i>"]
        ACL2["ACL<br/><i>Generic Adapter</i>"]
        D2["Domain Team<br/><i>Uses ACL</i>"]
        U2 --"Provides<br/>domain-friendly API"--> ACL2
        ACL2 --"May not fit<br/>domain perfectly"--> D2
    end
    
    Pro1["✓ Domain-specific translation<br/>✓ Team autonomy<br/>✗ Must learn external API"]
    Pro2["✓ Reduced downstream burden<br/>✓ Shared across consumers<br/>✗ Generic, not optimized"]
    
    D1 -.-> Pro1
    U2 -.-> Pro2

Downstream ownership gives domain teams control over translation but requires understanding the external system. Upstream ownership reduces burden but may not fit each domain’s specific needs.

Common Pitfalls

Pitfall 1: Anemic ACL That’s Just a Thin Wrapper Developers often create ACLs that are just thin wrappers around the external API, providing minimal translation and no semantic protection. The ACL might rename a few fields but still expose the external system’s structure, terminology, and constraints to the domain. Why it happens: Teams underestimate the semantic gap between systems, or they’re pressured to deliver quickly and skip the hard work of proper translation. They think “we’ll improve it later” but never do. How to avoid: Before implementing an ACL, explicitly document the semantic differences between your domain and the external system. List every concept that needs translation, every invariant that needs enforcement, and every external constraint that should be hidden. If your ACL implementation doesn’t address these differences, it’s not doing its job. Netflix’s ACL for legacy systems includes detailed documentation of semantic mappings, and code reviews specifically check that external concepts don’t leak into domain code.

Pitfall 2: Bidirectional Coupling Through Shared Models Teams sometimes try to use the same data models for both the domain and external system integration, thinking it will reduce duplication. They create a single Order class that’s used in domain logic and also serialized to call the external API. This creates bidirectional coupling—changes to the external API force changes to the domain model, and vice versa. Why it happens: Developers see duplication as waste and try to DRY (Don’t Repeat Yourself) across the ACL boundary. They don’t realize that some duplication is healthy when it enables independence. How to avoid: Embrace duplication across the ACL boundary. Have separate models for your domain and for external system integration, even if they look similar initially. The domain model should be optimized for business logic, while the external model should match the API contract. The ACL translates between them. Over time, these models will diverge, and you’ll be glad they’re separate. Stripe maintains completely separate internal and external models for payment objects, even though they started out nearly identical. This separation has allowed their internal model to evolve rapidly while maintaining API stability.

Pitfall 3: Leaking External Errors Into Domain Logic The ACL translates data structures but forgets to translate errors. When the external system returns an error, the ACL passes it through unchanged, forcing domain code to handle external error codes, retry logic, and failure modes. Why it happens: Error handling is often an afterthought, added hastily when the happy path is working. Teams focus on data translation but forget that errors are part of the contract. How to avoid: Design your ACL’s error handling as carefully as its data translation. Define domain-specific exceptions that represent business failures (like PaymentDeclined or InventoryUnavailable), and translate external errors into these domain exceptions. Hide external error codes, retry logic, and transient failures inside the ACL. Your domain should only see errors that represent meaningful business conditions. Amazon’s ACLs for payment processors translate hundreds of processor-specific error codes into a small set of domain exceptions like InsufficientFunds, InvalidCard, and ProcessorUnavailable, allowing domain logic to handle errors uniformly regardless of which processor is used.

Pitfall 4: ACL Becomes a God Object As more integration needs arise, teams pile functionality into the ACL until it becomes a massive, unmaintainable god object that handles translation, caching, retry logic, monitoring, rate limiting, and business logic. Why it happens: The ACL is a convenient place to add integration-related functionality, and without clear boundaries, it accumulates responsibilities. Teams don’t want to create “yet another layer,” so they stuff everything into the ACL. How to avoid: Keep the ACL focused on translation and semantic mapping. Other concerns like caching, rate limiting, and monitoring should be handled by separate components (perhaps using decorators or middleware). If you need business logic that coordinates multiple external systems, that belongs in a domain service, not the ACL. Each ACL should have a single responsibility: translating between one external system and your domain. Uber’s architecture separates ACLs (which only translate) from integration services (which coordinate multiple ACLs and add cross-cutting concerns) and domain services (which implement business logic). This separation keeps each component focused and maintainable.

Pitfall 5: Premature ACL for Internal Services Teams sometimes add ACLs between every microservice, even when those services are in the same bounded context and share the same domain model. This creates unnecessary overhead and complexity without providing value. Why it happens: Developers hear that ACLs are a best practice and apply them everywhere without understanding when they’re needed. Or they anticipate that services might diverge in the future and add ACLs “just in case.” How to avoid: Only add an ACL when there’s a genuine semantic boundary—when two systems have different models, different terminology, or different ownership. If two services are in the same bounded context and share a ubiquitous language, they can communicate directly using shared types. Don’t add ACLs speculatively; add them when the semantic gap becomes painful. Shopify’s microservices within the same domain (like order creation and order fulfillment) communicate directly without ACLs because they share the same order model. But services in different domains (like orders and inventory) use ACLs because they have different models and concerns.


Math & Calculations

Latency Impact Calculation

When evaluating whether to implement an ACL, you need to quantify the latency overhead it introduces. The ACL adds processing time for translation in both directions, plus any additional network hops if implemented as a separate service.

Formula:

Total_Latency = Base_External_Call + (2 × Translation_Time) + Network_Overhead

Where:
- Base_External_Call = latency of calling external system directly
- Translation_Time = time to translate one request or response
- Network_Overhead = additional latency if ACL is a separate service (0 if in-process)

Worked Example: Suppose you’re integrating with a legacy payment processor:

  • Base external call latency: 150ms (network + processing)
  • Translation time: 5ms per direction (JSON parsing, field mapping, validation)
  • Network overhead: 2ms (if ACL is a separate service)
Direct Integration Latency = 150ms

ACL Integration Latency = 150ms + (2 × 5ms) + 2ms = 162ms

Overhead = 162ms - 150ms = 12ms (8% increase)

For a system handling 1,000 payment requests per second, this 12ms overhead means:

  • Additional processing capacity needed: 12ms × 1,000 = 12 seconds of CPU time per second
  • This requires at least 12 CPU cores dedicated to ACL translation

However, this overhead must be weighed against the cost of domain corruption. If avoiding the ACL means your domain code becomes 20% more complex (harder to test, slower to modify), the 8% latency cost is usually worth it. Stripe’s analysis showed that their ACL overhead was 15ms per payment, but it enabled them to switch payment processors in 2 weeks instead of 6 months, saving millions in engineering costs.

Throughput Impact Calculation

The ACL can become a bottleneck if not properly scaled. Calculate the maximum throughput:

Formula:

Max_Throughput = (Number_of_ACL_Instances × CPU_Cores_per_Instance) / Translation_Time

Where:
- Number_of_ACL_Instances = deployed instances of ACL service
- CPU_Cores_per_Instance = cores available for translation
- Translation_Time = time to translate one request (in seconds)

Worked Example: For an ACL service with:

  • 5 instances deployed
  • 4 cores per instance
  • 5ms (0.005s) translation time per request
Max_Throughput = (5 × 4) / 0.005 = 20 / 0.005 = 4,000 requests/second

If your peak traffic is 3,000 requests/second, you have 33% headroom. But if traffic spikes to 5,000 requests/second, the ACL becomes a bottleneck. You’d need to scale to 7 instances to handle the load with the same headroom.


Real-World Examples

Example 1: Stripe’s Payment Processor ACLs

Stripe integrates with dozens of payment processors worldwide (Visa, Mastercard, PayPal, Alipay, etc.), each with completely different APIs, data models, and business rules. Instead of letting these differences pollute their core payment engine, Stripe implements a separate ACL for each processor. Their internal domain uses concepts like PaymentIntent, Charge, and Refund with clean, consistent semantics. Each processor ACL translates these domain concepts into processor-specific API calls.

For example, when a merchant creates a PaymentIntent, Stripe’s domain logic validates the request, checks fraud rules, and emits a domain event. The Visa ACL subscribes to this event and translates it into Visa’s authorization request format, which uses completely different field names and requires additional data like AVS (Address Verification System) codes. When Visa responds, the ACL translates the response back into Stripe’s domain model, mapping Visa’s dozens of decline codes into Stripe’s standardized error taxonomy.

The interesting detail: Stripe’s ACLs are versioned independently of their core API. When a payment processor changes their API (which happens frequently), Stripe updates only the relevant ACL, leaving their domain and customer-facing API unchanged. This architecture enabled Stripe to add support for 15 new payment methods in 2023 without any changes to their core payment engine. The ACL pattern is so central to their architecture that new engineers spend their first week building a mock ACL for a fictional payment processor to learn the pattern.

Example 2: Netflix’s Legacy Billing System Integration

When Netflix transitioned from a monolithic architecture to microservices, they faced a challenge: dozens of new microservices needed to interact with a legacy billing system that was built in the 1990s. This system used a proprietary protocol, stored data in a denormalized format optimized for batch processing, and had business logic baked into stored procedures. Rather than forcing every microservice team to understand this complexity, Netflix built a dedicated ACL service called “Billing Gateway.”

The Billing Gateway exposes a modern REST API that speaks Netflix’s domain language—concepts like Subscription, Plan, PaymentMethod, and Invoice. Internally, it translates these into the legacy system’s data model, which uses concepts like ACCT_MSTR, BILL_CYC, and PMT_METH_CD. The ACL handles complex translation logic: for example, Netflix’s domain allows customers to have multiple payment methods with priorities, but the legacy system only supports one payment method per account. The ACL maintains a mapping table and orchestrates multiple legacy system calls to simulate the domain’s behavior.

The interesting detail: The Billing Gateway includes a sophisticated caching layer because the legacy system is slow (200-500ms per query). The ACL caches frequently accessed data like subscription status and payment methods, serving 95% of requests from cache with <10ms latency. This caching is transparent to consumers—they just call the clean domain API and get fast responses. When Netflix eventually replaces the legacy billing system, they’ll only need to reimplement the Billing Gateway’s internals; all consuming services will continue working unchanged. This ACL has been running in production for over 8 years, handling billions of requests per month.

Example 3: Shopify’s Multi-Channel Inventory ACLs

Shopify merchants sell products across multiple channels—their Shopify store, Amazon, eBay, Facebook Marketplace, and physical retail locations. Each channel has its own inventory management system with different concepts, APIs, and update frequencies. Shopify’s core inventory domain needs a unified view of inventory across all channels, but it can’t be coupled to each channel’s specific model.

Shopify implements a separate ACL for each sales channel. Their domain defines a clean InventoryLevel concept with fields like productId, locationId, available, and committed. Each channel ACL translates this into channel-specific formats: Amazon’s ACL converts it into FBA (Fulfillment by Amazon) inventory updates, eBay’s ACL maps it to eBay’s quantity and location model, and the POS (Point of Sale) ACL syncs it with in-store inventory systems.

The translation isn’t just data format conversion—it includes business logic differences. Amazon requires inventory to be allocated to specific fulfillment centers, while eBay uses a simpler single-location model. Shopify’s domain doesn’t care about these differences; the ACLs handle them. When a product sells on Amazon, the Amazon ACL receives a webhook, translates it into a domain InventoryAdjusted event, and publishes it to Shopify’s event bus. Other services consume this domain event without knowing it originated from Amazon.

The interesting detail: Shopify’s channel ACLs implement sophisticated conflict resolution. If a product sells simultaneously on Shopify and Amazon, both ACLs try to decrement inventory. The ACLs use optimistic locking and event timestamps to detect conflicts, then apply business rules (like “Amazon sales take priority because they have stricter SLAs”) to resolve them. This conflict resolution logic is hidden inside the ACLs, keeping the core inventory domain simple and focused on the happy path. Shopify’s ACL architecture has scaled to support over 100 different sales channels and integration partners, with new channels added monthly without touching the core inventory service.

Stripe’s Multi-Processor ACL Architecture

graph TB
    subgraph Stripe Domain
        Core["Payment Engine<br/><i>Domain Logic</i>"]
        PI["PaymentIntent<br/><i>Domain Model</i>"]
    end
    
    subgraph ACL Layer
        VisaACL["Visa ACL<br/><i>Authorization Format</i>"]
        PayPalACL["PayPal ACL<br/><i>Express Checkout</i>"]
        AlipayACL["Alipay ACL<br/><i>QR Code Flow</i>"]
    end
    
    subgraph External Processors
        Visa["Visa Network<br/><i>Card Processing</i>"]
        PayPal["PayPal API<br/><i>Wallet Payment</i>"]
        Alipay["Alipay API<br/><i>Mobile Payment</i>"]
    end
    
    Core --"1. Create PaymentIntent"--> PI
    PI --"2. Route by method"--> VisaACL
    PI --"2. Route by method"--> PayPalACL
    PI --"2. Route by method"--> AlipayACL
    
    VisaACL --"3. Translate to<br/>auth request + AVS"--> Visa
    PayPalACL --"3. Translate to<br/>checkout session"--> PayPal
    AlipayACL --"3. Translate to<br/>QR code request"--> Alipay
    
    Visa --"4. Response"--> VisaACL
    PayPal --"4. Response"--> PayPalACL
    Alipay --"4. Response"--> AlipayACL
    
    VisaACL --"5. Standardized<br/>PaymentResult"--> Core
    PayPalACL --"5. Standardized<br/>PaymentResult"--> Core
    AlipayACL --"5. Standardized<br/>PaymentResult"--> Core

Stripe maintains separate ACLs for each payment processor, translating their unified PaymentIntent model into processor-specific formats. This allows adding new processors without changing core payment logic.


Interview Expectations

Mid-Level

What You Should Know: At the mid-level, you should understand that the Anti-Corruption Layer is a translation boundary that prevents external system models from polluting your domain. You should be able to explain the basic problem it solves: when integrating with legacy systems or third-party APIs, you don’t want their terminology, data structures, and constraints leaking into your clean domain model. You should know that the ACL acts as an adapter or facade that translates requests and responses bidirectionally.

You should be able to describe a simple implementation: the domain defines interfaces using its own types, and the ACL implements these interfaces by calling the external system and translating the results. You should understand that the ACL is owned by the team that owns the domain being protected, not the external system’s team. You should recognize when an ACL is needed versus when direct integration is acceptable—generally, you need an ACL when there’s a significant semantic gap between systems.

Bonus Points: Discuss a specific example from your experience where you implemented or worked with an ACL. Explain the semantic differences you were translating (not just field name mappings, but conceptual differences). Mention that the ACL is related to the adapter pattern from Gang of Four, but with a specific focus on protecting domain purity. Show awareness that the ACL adds latency and complexity, and explain how you’d decide whether that tradeoff is worth it. Reference Domain-Driven Design and Eric Evans if you’re familiar with the origin of the pattern.

Common Mistakes to Avoid: Don’t describe the ACL as just a “wrapper” or “API client”—emphasize the semantic translation aspect. Don’t suggest using the same data models on both sides of the ACL (that defeats the purpose). Don’t claim you’d use an ACL everywhere—show judgment about when it’s needed versus overkill. Don’t ignore the performance implications or suggest the ACL is “free.”


Senior Level:

What You Should Know: As a senior engineer, you should deeply understand the strategic architectural value of the ACL pattern. You should be able to explain how it enables independent evolution of systems, allowing your domain to change without coordinating with external dependencies, and vice versa. You should discuss the ACL in the context of bounded contexts from Domain-Driven Design, explaining that it marks the boundary between contexts with different ubiquitous languages.

You should know multiple implementation variants (facade, adapter, service-based, event-driven, repository-based) and be able to choose the right one based on context. You should understand the tradeoffs deeply: architectural purity versus performance, translation complexity versus model divergence, synchronous versus asynchronous translation. You should be able to design an ACL that handles not just data translation but also error translation, ensuring external error codes and failure modes don’t leak into domain logic.

You should know how to handle complex translation scenarios: aggregating multiple external calls into a single domain operation, enriching external data from other sources to meet domain invariants, and implementing bidirectional translation that preserves domain semantics even when the external system has different rules. You should understand ownership models and be able to argue for why the downstream team should typically own the ACL.

Bonus Points: Discuss how you’ve used ACLs to enable parallel team development or to facilitate a legacy system migration. Explain how you’d measure the cost of an ACL (latency overhead, maintenance burden) and compare it to the cost of domain corruption. Mention specific companies’ architectures (Stripe, Netflix, Shopify) and how they use ACLs at scale. Discuss how ACLs interact with other patterns like CQRS, event sourcing, or saga patterns. Show awareness of when NOT to use an ACL—for example, between services in the same bounded context, or for simple CRUD operations where domain purity isn’t critical.

Common Mistakes to Avoid: Don’t oversimplify the pattern as just “good separation of concerns.” Don’t claim ACLs are always necessary at every service boundary (show judgment). Don’t ignore the operational complexity of maintaining translation logic as systems evolve. Don’t suggest that the ACL should contain business logic—it should only translate, not make business decisions. Don’t fail to discuss error handling and edge cases.


Staff-Plus Level:

What You Should Know: At the staff-plus level, you should be able to architect ACL strategies for an entire organization. You should understand how to balance consistency (shared ACL services that multiple teams use) versus autonomy (each team implements their own ACL). You should be able to design governance models: who decides when an ACL is required, who reviews ACL implementations to ensure they’re not leaky abstractions, and how to prevent ACLs from becoming dumping grounds for integration logic.

You should be able to discuss the organizational implications of ACLs. They enable team independence and parallel development, but they also create coordination challenges when multiple teams integrate with the same external system. You should know how to design shared ACL services that serve multiple consumers without becoming lowest-common-denominator compromises. You should understand how ACLs fit into a broader modernization strategy—for example, using ACLs to gradually strangle a legacy monolith while building new microservices.

You should be able to evaluate when the ACL pattern is insufficient and a more sophisticated integration strategy is needed—perhaps an integration platform, an event mesh, or a complete re-platforming. You should understand the economic tradeoffs: the ACL is an investment in long-term maintainability that pays off over years, but it has upfront costs in development time and ongoing costs in latency and operational complexity.

Distinguishing Signals: You’ve led a large-scale migration where ACLs were critical to success, and you can discuss the organizational challenges, not just the technical ones. You can explain how you’ve coached teams on when to use ACLs and how to implement them well, perhaps through architecture reviews or design patterns documentation. You understand the relationship between ACLs and other strategic patterns like bounded contexts, context mapping, and evolutionary architecture. You can discuss failure modes you’ve seen: ACLs that became god objects, ACLs that were too thin and didn’t provide real protection, or premature ACLs that added complexity without value. You can articulate clear principles for when ACLs are worth the investment versus when simpler integration approaches suffice.

Common Interview Questions

Question 1: When would you use an Anti-Corruption Layer versus direct integration?

Concise Answer (60 seconds): Use an ACL when there’s a significant semantic gap between your domain and the external system—different terminology, different data models, or different business rules. The ACL is worth the overhead when your domain is a core competitive advantage that needs to evolve independently, or when the external system is legacy/third-party and likely to change. Use direct integration when systems share the same domain model (like services in the same bounded context), when the integration is simple CRUD with no semantic translation needed, or when performance is critical and the integration is stable. The key question is: will this external system’s model corrupt my domain if it leaks in?

Detailed Answer (2 minutes): The decision hinges on three factors: semantic gap, domain criticality, and stability. First, assess the semantic gap. If the external system uses fundamentally different concepts—like a legacy ERP that thinks in “account masters” while your domain thinks in “customers”—you need an ACL. But if it’s just minor field name differences, a simple adapter might suffice. Second, consider domain criticality. If this domain is central to your business and will evolve rapidly, protect it with an ACL. If it’s peripheral (like an internal admin tool), direct integration is fine. Third, evaluate stability. If the external system is a legacy monolith that changes rarely, the ACL’s translation logic will be stable. If it’s a fast-moving third-party API, the ACL might require frequent updates, increasing maintenance burden.

I’d also consider team boundaries. If the external system is owned by another team with different priorities, an ACL gives you independence—you can evolve your domain without coordinating with them. But if it’s owned by your team or a closely aligned team, direct integration might be simpler. Finally, measure the cost. An ACL adds 10-50ms latency and requires ongoing maintenance. If your domain is read-heavy with strict latency requirements, that cost might be prohibitive. But if it’s write-heavy or latency-tolerant, the cost is usually worth the architectural benefits.

Red Flags:

  • Saying “always use an ACL for best practices” without considering tradeoffs
  • Claiming the ACL is “free” or has no performance impact
  • Not mentioning semantic differences—focusing only on technical integration
  • Suggesting you’d use an ACL between every microservice
  • Not considering team ownership and organizational factors

Question 2: How would you implement an ACL for a legacy system that has poor data quality?

Concise Answer (60 seconds): The ACL must enforce your domain’s invariants even when the legacy system allows invalid data. Implement validation at the ACL boundary, rejecting or enriching data that doesn’t meet domain requirements. For missing required fields, either fetch them from other sources, apply sensible defaults, or fail fast with a clear error. For inconsistent data, implement normalization logic in the ACL. Use defensive programming—assume the legacy system will return garbage and handle it gracefully. Log data quality issues for monitoring, and consider implementing a feedback loop to improve the legacy system over time.

Detailed Answer (2 minutes): Start by explicitly documenting your domain’s invariants—what must be true for your domain to function correctly. For example, if your domain requires every order to have a valid email address, but the legacy system allows nulls, the ACL must handle this mismatch. You have several options: reject invalid data with a clear error (forcing upstream fixes), enrich data from another source (like a customer service that has the email), or apply a default (like a placeholder email for legacy orders). The right choice depends on business requirements.

Implement multi-layered validation: first, validate that the legacy system’s response is structurally valid (all expected fields present, correct types). Second, validate business rules (email format, date ranges, referential integrity). Third, validate domain invariants (order total matches line items, status transitions are valid). Each layer should have specific error handling—structural errors might indicate a legacy system bug, while business rule violations might be expected and need graceful handling.

For data normalization, the ACL should standardize inconsistencies. If the legacy system uses different date formats in different fields, normalize them all to ISO 8601. If it uses inconsistent status codes (“COMPLETE” vs “COMPLETED” vs “DONE”), map them to your domain’s canonical values. Implement comprehensive logging and metrics to track data quality issues—what percentage of records have missing emails, how often do you need to apply defaults, etc. This visibility helps you prioritize improvements to the legacy system and proves the value of the ACL to stakeholders.

Red Flags:

  • Suggesting you’d let invalid data into your domain and handle it there (defeats the ACL’s purpose)
  • Not discussing validation strategies or error handling
  • Assuming the legacy system will be fixed soon (it won’t)
  • Not mentioning logging and monitoring of data quality issues
  • Proposing to fix the legacy system instead of protecting your domain

Question 3: How do you handle versioning when the external system’s API changes?

Concise Answer (60 seconds): The ACL should isolate your domain from external API changes. When the external system releases a new API version, update only the ACL implementation, leaving your domain unchanged. Implement version detection in the ACL—either through configuration or runtime detection—so you can support multiple external API versions simultaneously during migrations. Use feature flags to gradually roll out the new version. If the API change requires domain model changes, that’s a sign your ACL wasn’t providing enough abstraction—you may need to redesign it to better isolate the domain.

Detailed Answer (2 minutes): The ACL’s primary value is enabling independent evolution, so external API changes should be absorbed by the ACL without affecting the domain. When the external system announces an API change, first assess whether it’s purely technical (new endpoints, different authentication) or semantic (different data model, changed business rules). Technical changes should be completely hidden by the ACL. Semantic changes might require domain changes, but even then, the ACL should minimize the impact.

Implement a versioning strategy in the ACL. One approach is to support multiple external API versions simultaneously using a strategy pattern—the ACL detects which version to use (via configuration or by inspecting responses) and delegates to the appropriate implementation. This allows gradual migration: you can test the new version in staging while production still uses the old version, then gradually roll out the new version using feature flags or canary deployments.

For breaking changes that do require domain updates, use the strangler pattern: create a new ACL implementation that translates the new external API into your existing domain model, run both ACLs in parallel (perhaps with shadow traffic), verify they produce equivalent results, then cut over. If the external API change is so fundamental that it can’t be mapped to your existing domain model, that’s a signal that your domain model might need to evolve—but this should be rare if the ACL was well-designed initially.

Maintain comprehensive integration tests for the ACL that run against both old and new API versions. These tests should verify that the ACL correctly translates all edge cases and error conditions. When you deprecate support for an old API version, these tests ensure you haven’t broken anything. Document the version support policy clearly—how long will you support old versions, what’s the migration timeline, etc.

Red Flags:

  • Suggesting domain code should check API versions or handle version differences
  • Not mentioning a gradual migration strategy
  • Claiming you’d update the domain every time the external API changes
  • Not discussing testing strategies for version compatibility
  • Proposing to maintain separate ACLs for each version indefinitely (technical debt)

Question 4: What’s the difference between an ACL and a simple adapter or facade?

Concise Answer (60 seconds): The ACL is a specific application of adapter and facade patterns with a focus on semantic translation and domain protection. A simple adapter converts one interface to another but might still expose external concepts. A facade simplifies a complex interface but doesn’t necessarily translate semantics. The ACL does both: it translates external concepts into domain concepts AND enforces domain invariants. The key difference is intent—the ACL’s purpose is to prevent corruption of your domain model, not just to simplify integration. It’s a strategic architectural pattern, not just a tactical coding pattern.

Detailed Answer (2 minutes): Adapters and facades are structural patterns from the Gang of Four that focus on interface compatibility. An adapter makes one interface compatible with another—like adapting a European plug to a US outlet. A facade provides a simplified interface to a complex subsystem—like a home theater remote that hides the complexity of controlling multiple devices. The ACL uses these patterns but adds a semantic layer.

The critical difference is semantic translation. A simple adapter might translate a getCustomerById() call into the external system’s fetchAccountByNumber() call, but it might still return the external system’s Account object with its terminology and structure. The ACL would translate that Account into your domain’s Customer object, mapping fields, converting types, and enforcing domain rules. If the external Account allows null emails but your domain requires emails, the ACL handles this mismatch—a simple adapter wouldn’t.

The ACL also has a defensive posture. It assumes the external system is hostile or at least untrustworthy, and it validates everything crossing the boundary. A facade might assume the subsystem is well-behaved and just simplify the interface. The ACL validates responses, handles errors defensively, and ensures domain invariants are maintained even if the external system returns garbage.

Finally, the ACL is a strategic pattern tied to Domain-Driven Design and bounded contexts. It marks the boundary between contexts with different ubiquitous languages. An adapter or facade might be used within a single context just for convenience. The ACL is specifically about protecting one context from another’s influence. In practice, you might implement an ACL using adapter and facade patterns, but the ACL concept is broader—it’s about architectural boundaries and domain integrity, not just code structure.

Red Flags:

  • Saying they’re the same thing or just different names
  • Not mentioning semantic translation or domain protection
  • Focusing only on code structure without discussing architectural intent
  • Not connecting ACL to Domain-Driven Design or bounded contexts
  • Claiming adapters and facades don’t do translation (they can, but it’s not their primary purpose)

Question 5: How would you test an Anti-Corruption Layer?

Concise Answer (60 seconds): Test the ACL at multiple levels. Unit tests verify translation logic in isolation using mocks for the external system. Integration tests verify the ACL works with the real external system (or a realistic test double). Contract tests ensure the ACL’s interface matches what the domain expects. End-to-end tests verify the full flow through domain and ACL. Focus testing on edge cases: null values, missing fields, error conditions, and boundary values. Test both directions—domain to external and external to domain. Use property-based testing to generate random inputs and verify invariants are maintained.

Detailed Answer (2 minutes): Start with unit tests for the translation logic. These tests should mock the external system and verify that the ACL correctly translates requests and responses. Test every field mapping, every type conversion, every default value. Critically, test edge cases: what happens when the external system returns null for a required field? When it returns an unexpected status code? When it returns malformed data? The ACL should handle these gracefully, either by applying defaults, enriching from other sources, or failing with clear errors. These unit tests should be fast and comprehensive—aim for 100% coverage of translation logic.

Integration tests verify the ACL works with the real external system. If possible, run these against a test environment of the external system. If not, use a realistic test double (like WireMock or Pact) that simulates the external system’s behavior, including error conditions and edge cases. These tests verify that your understanding of the external API is correct and that the ACL handles real-world responses. Run these tests in CI, but they might be slower and more brittle than unit tests.

Contract tests ensure the ACL’s interface matches what the domain expects. Use consumer-driven contract testing (like Pact) where the domain defines the contract it expects from the ACL, and the ACL implementation is tested against this contract. This catches breaking changes early—if the ACL changes its interface, contract tests fail before the domain is affected. This is especially valuable in microservices architectures where the domain and ACL might be deployed independently.

Property-based testing is powerful for ACLs. Use libraries like Hypothesis (Python) or QuickCheck (Haskell/Scala) to generate random inputs and verify invariants. For example, verify that for any valid domain object, translating it to external format and back yields an equivalent object. Verify that the ACL never returns data that violates domain invariants, regardless of what the external system returns. This catches edge cases you might not think to test manually.

Finally, test error handling thoroughly. Simulate every error condition the external system might return—timeouts, 500 errors, malformed responses, rate limiting, authentication failures. Verify the ACL translates these into appropriate domain exceptions and doesn’t leak external error details. Test retry logic, circuit breakers, and fallback behavior if the ACL includes these concerns.

Red Flags:

  • Only mentioning unit tests without discussing integration or contract tests
  • Not emphasizing edge case and error condition testing
  • Suggesting you’d test the ACL through the domain (should test in isolation too)
  • Not mentioning property-based testing or invariant verification
  • Claiming the ACL doesn’t need much testing because it’s “just translation”