Context Mapping — Deep Dive

Level: Intermediate → Advanced
Pre-reading: 02 · Domain-Driven Design · 02.01 · Strategic DDD


What is a Context Map?

A context map documents the relationships between bounded contexts. It makes explicit how contexts integrate, who has power in the relationship, and what translation is needed.

graph LR
    ORD[Order Context] -->|Events| INV[Inventory Context]
    ORD -->|API| PAY[Payment Context]
    PAY -->|ACL| LEGACY[Legacy Billing]
    SHIP[Shipping Context] -->|Conformist| EXT[External Carrier API]
    AUTH[Auth Context] -->|Open Host| ORD
    AUTH -->|Open Host| PAY

Context Mapping Patterns

Upstream / Downstream Relationships

In any integration between two contexts, one is upstream (producer) and one is downstream (consumer). Power dynamics affect how contracts evolve.

Relationship Upstream Downstream
Customer-Supplier Shapes the contract Can negotiate changes
Conformist Dictates the contract Must accept as-is
Open Host Service Publishes stable API Multiple consumers use it
Anti-Corruption Layer Any Translates to protect its model

Pattern Details

1. Shared Kernel

Two contexts share a subset of the domain model. Changes require joint agreement from both teams.

graph TD
    subgraph Context A
        A1[Model A]
    end
    subgraph Context B
        B1[Model B]
    end
    subgraph Shared Kernel
        SK[Shared Types]
    end
    A1 --> SK
    B1 --> SK
Pros Cons
Avoids duplication Tight coupling
Consistent types Coordination overhead
Useful for transitioning Can grow uncontrollably

Use when: Transitioning from monolith; very closely related contexts; temporary state.

Avoid when: Teams are autonomous; shared kernel keeps growing.

Shared Kernel should shrink over time

If your shared kernel grows, you're building a distributed monolith. Work to eliminate it or clearly define what belongs there.


2. Customer-Supplier

The downstream (customer) context depends on the upstream (supplier) context. The relationship allows negotiation — downstream can request changes.

sequenceDiagram
    participant DS as Downstream - Customer
    participant US as Upstream - Supplier
    DS->>US: Request new field in API
    US->>US: Evaluate feasibility
    US->>DS: Implement and release
Upstream Responsibility Downstream Responsibility
Maintain stable API Provide feedback on needs
Communicate breaking changes Adapt to changes
Consider downstream needs Accept that changes take time

Use when: Clear producer-consumer relationship with collaboration between teams.


3. Conformist

Downstream has no power to influence upstream. Accept the upstream model as-is, conform to it.

graph LR
    US[Upstream - External API]
    DS[Downstream - Your Service]
    US -->|Dictates| DS
Scenario Example
Third-party API Stripe, Twilio, Google Maps
Regulated standard ISO, SWIFT, HL7
Dominant partner Large retailer dictating EDI format

Use when: You have no negotiating power; the upstream is outside your control.

Mitigation: Consider adding an Anti-Corruption Layer if the upstream model leaks into your domain.


4. Anti-Corruption Layer (ACL)

A translation layer that protects your domain model from external or legacy models. All interactions with the upstream context go through the ACL.

graph LR
    subgraph Your Context
        DM[Domain Model]
        ACL[Anti-Corruption Layer]
    end
    EXT[External/Legacy System]
    DM --> ACL
    ACL --> EXT
ACL Responsibility
Translate external DTOs to domain objects
Translate domain commands to external API calls
Hide external model complexity from domain
Handle external error formats

ACL Implementation

// External model (messy, not your design)
public class LegacyOrderResponse {
    private String ord_id;
    private int stat_code;  // 1=new, 2=shipped, 3=cancelled
    private List<LegacyItem> itms;
}

// Your domain model (clean)
public class Order {
    private OrderId id;
    private OrderStatus status;
    private List<OrderLine> lines;
}

// ACL translates between them
public class OrderAcl {
    public Order toDomain(LegacyOrderResponse response) {
        return new Order(
            new OrderId(response.getOrd_id()),
            mapStatus(response.getStat_code()),
            mapLines(response.getItms())
        );
    }

    private OrderStatus mapStatus(int code) {
        return switch (code) {
            case 1 -> OrderStatus.NEW;
            case 2 -> OrderStatus.SHIPPED;
            case 3 -> OrderStatus.CANCELLED;
            default -> throw new IllegalArgumentException("Unknown status: " + code);
        };
    }
}

Use when: Integrating with legacy systems, external APIs, or any system with a model you don't want to adopt.


5. Open Host Service

Upstream exposes a well-defined protocol (API) that multiple downstream consumers can use. The API is stable and documented.

graph TD
    OHS[Open Host Service - Auth API]
    C1[Consumer 1 - Order Service]
    C2[Consumer 2 - Payment Service]
    C3[Consumer 3 - Mobile App]
    OHS --> C1
    OHS --> C2
    OHS --> C3
Characteristics
Versioned API (OpenAPI, gRPC)
Stable; breaking changes are rare
Documentation and SDK provided
Often paired with Published Language

Use when: Building a platform service used by multiple consumers.


6. Published Language

A well-documented, shared schema for data exchange. Often paired with Open Host Service.

Format Use Case
JSON Schema REST APIs
Avro Kafka events (Schema Registry)
Protocol Buffers gRPC services
XML Schema (XSD) Enterprise integrations

Use when: Integration contracts need formal documentation and validation.


7. Separate Ways

No integration. Each context solves the problem independently.

When Separate Ways is Right
Integration cost exceeds benefit
Contexts have fundamentally different needs
Duplication is acceptable

Example: Two contexts both need currency conversion. If integrating is complex and requirements differ, each implements its own.


Context Map Visualization

Draw your context map to make relationships explicit:

graph TB
    subgraph Legend
        L1[Upstream]
        L2[Downstream]
        L1 -->|U/D| L2
    end

    AUTH[Auth Service OHS]
    ORDER[Order Service]
    PAY[Payment Service]
    INV[Inventory Service]
    LEG[Legacy ERP]
    SHIP[Shipping Carrier]

    AUTH -->|OHS| ORDER
    AUTH -->|OHS| PAY
    ORDER -->|Customer-Supplier| INV
    ORDER -->|Events| PAY
    INV -->|ACL| LEG
    ORDER -->|Conformist| SHIP

Context Map Heuristics

Relationship Draw As
Customer-Supplier Arrow with label "C/S"
Conformist Arrow with label "Conformist"
ACL Box around downstream with "ACL"
Open Host Service Labeled API box
Shared Kernel Overlapping area
Separate Ways No connection

Integration Strategy by Relationship

Relationship Recommended Integration
Shared Kernel Shared library or schema
Customer-Supplier API with versioning
Conformist Direct API call (no translation)
ACL Adapter/translator layer
Open Host Service REST/gRPC with OpenAPI/Proto
Published Language Schema Registry for events

Common Mistakes

Mistake Impact Fix
No explicit context map Hidden dependencies Document relationships
Shared Kernel grows Tight coupling Extract or eliminate
Missing ACL for legacy Legacy model leaks into domain Add translation layer
Conformist to internal service Downstream blocked by upstream Negotiate (Customer-Supplier)
Too many Shared Kernels Distributed monolith One Shared Kernel max; prefer APIs

When should you use an Anti-Corruption Layer?

Use ACL when integrating with (1) legacy systems with poor models, (2) external APIs you can't control, (3) any system whose model you don't want leaking into your domain. ACL has a cost (translation code, potential bugs), so only use it when the upstream model is significantly different from your domain model.

What's the difference between Customer-Supplier and Conformist?

Customer-Supplier implies negotiation power — downstream can request changes. Conformist means downstream has no power and must accept whatever upstream provides. With third-party APIs (Stripe, AWS), you're a Conformist. With internal teams, aim for Customer-Supplier so needs are considered.

How do you evolve from Shared Kernel to separate contexts?

(1) Identify which parts of the shared kernel are truly shared vs. duplicated for convenience. (2) For truly shared types (e.g., Money), consider a small shared library. (3) For context-specific types, duplicate and let them evolve independently. (4) Replace shared database access with APIs or events. (5) Gradually shrink the kernel until it's eliminated or minimal.