Skip to main content
DDD Without the Book Club

Context Maps in a 50-Service Org

Ravinder··6 min read
DDDDomain-Driven DesignArchitectureContext MapMicroservices
Share:
Context Maps in a 50-Service Org

At five services, one architect holds the full picture. At fifteen, you need a wiki page. At fifty, you need a context map — or you need to hire a second architect just to remember which team owns what, and then hire a third to mediate the resulting coupling arguments.

A context map is not an architecture diagram in the enterprise sense. It's a map of teams, models, and relationships. It tells you who is upstream, who is downstream, what crosses the boundary, and what kind of relationship the teams have. In a 50-service organisation, it's the document that stops new engineers from creating the coupling that will hurt you in six months.

What Goes on a Context Map

A context map shows:

  1. Bounded contexts (boxes) — named after the domain, not the service
  2. Relationships (arrows) — upstream to downstream
  3. Integration patterns (labels) — what kind of relationship

The relationship types that matter most in practice:

Pattern Meaning
Partnership Two teams co-evolve models together. High coordination.
Shared Kernel Both contexts share a subset of the domain model. Requires joint governance.
Customer/Supplier Upstream team (supplier) provides what downstream team (customer) needs. Formal agreement.
Conformist Downstream team conforms to upstream's model — no translation, no power to negotiate.
Anti-Corruption Layer Downstream team translates upstream's model to protect its own domain.
Open Host / Published Language Upstream publishes a stable protocol all downstream consumers use.
Separate Ways No integration — contexts are truly independent.
graph TD subgraph Platform["Platform Team"] IAM[Identity &\nAccess Context] NOTIFY[Notification\nContext] end subgraph Commerce["Commerce Team"] CATALOG[Catalog\nContext] CART[Cart\nContext] ORDER[Order\nContext] end subgraph Finance["Finance Team"] BILLING[Billing\nContext] LEDGER[Ledger\nContext] end subgraph Fulfillment["Fulfillment Team"] INVENTORY[Inventory\nContext] SHIPPING[Shipping\nContext] end subgraph External ERP[Legacy ERP\nExternal System] end IAM -- "OHS/PL: JWT tokens" --> ORDER IAM -- "OHS/PL: JWT tokens" --> BILLING CATALOG -- "C/S: ProductCreated events" --> CART CATALOG -- "C/S: ProductCreated events" --> INVENTORY ORDER -- "C/S: OrderConfirmed event" --> BILLING ORDER -- "C/S: OrderConfirmed event" --> INVENTORY ORDER -- "C/S: OrderConfirmed event" --> NOTIFY BILLING -- "C/S: PaymentCaptured event" --> LEDGER INVENTORY -- "C/S: StockAllocated event" --> SHIPPING INVENTORY -- "ACL" --> ERP

The Relationship That Matters Most: Upstream/Downstream

In a 50-service org, the most important thing a context map reveals is power. The upstream team makes decisions that downstream teams must live with. If the upstream team changes their event schema, downstream teams absorb the cost.

Document this explicitly. When an upstream team says "we're renaming productCode to sku in our events next month," downstream teams need time to adapt. The context map is what tells you which teams to notify.

// Context map encoded in code: a CONTEXT.md in each service repo
/*
 * Service: Order Service
 * Bounded Context: Order Context
 * Team: Commerce Team
 *
 * UPSTREAM DEPENDENCIES (we conform to):
 * - Identity Context (IAM Team): JWT validation via OHS
 * - Catalog Context (Commerce Team): ProductCreated / ProductUpdated events [Partnership]
 *
 * DOWNSTREAM CONSUMERS (they conform to us, or use ACL):
 * - Billing Context (Finance Team): consumes OrderConfirmed [Customer/Supplier]
 * - Inventory Context (Fulfillment Team): consumes OrderConfirmed [Customer/Supplier]
 * - Notification Context (Platform Team): consumes OrderConfirmed [Conformist]
 *
 * PUBLISHED EVENTS:
 * - OrderConfirmed v2 (see events/OrderConfirmed.schema.json)
 * - OrderCancelled v1 (see events/OrderCancelled.schema.json)
 *
 * SHARED KERNEL:
 * - package: @company/shared-kernel (OrderId, CustomerId, Money, Currency)
 */

This comment block, kept in CONTEXT.md or SERVICE.md, gives new engineers the context they need without reading 50 service READMEs.

Keeping the Map Current

The context map rots the moment you stop updating it. In a 50-service org, that happens fast. The approaches that actually work in practice:

Embed it near the code. A CONTEXT.md per service repository is updated in the same PR that adds an integration. Architecture diagrams in Confluence get updated never.

Make it a PR checklist item. "Does this PR add or change an integration? Update CONTEXT.md." Three seconds of awareness, not a quarterly architecture review.

Use contract tests as living documentation. Consumer-driven contract tests (Pact is the standard tool) encode which events and APIs a consumer depends on. When the provider changes their schema, the contract test fails — the map doesn't need manual updating for schema changes.

// Pact consumer test: defines what Order Context expects from Catalog Context
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "catalog-service")
class OrderCatalogContractTest {
 
    @Pact(consumer = "order-service")
    public MessagePact productCreatedPact(MessagePactBuilder builder) {
        return builder
            .expectsToReceive("a ProductCreated event")
            .withContent(new PactDslJsonBody()
                .stringType("productId")
                .stringType("sku")
                .stringType("name")
                .numberType("basePrice")
                .stringType("currency")
            )
            .toPact();
    }
 
    @Test
    @PactTestFor(pactMethod = "productCreatedPact", providerType = ProviderType.ASYNCH)
    void consumesProductCreatedEvent(List<Message> messages) {
        ProductCreatedEvent event = parseEvent(messages.get(0));
        // Verifies our consumer can parse what the provider says it sends
        assertThat(event.getSku()).isNotBlank();
    }
}

When Catalog changes their event schema, this test catches it before the PR merges. The contract is the live map.

Identifying Coupling Problems

The patterns that signal coupling problems on a context map:

Fan-out from a single context. If one bounded context has 20 downstream consumers, it's either a platform service (expected) or a hidden monolith (problematic). Every change to that context requires coordinating with 20 teams.

Circular dependencies. Order calls Billing, Billing calls Order. This is a context boundary error — one of them is in the wrong place. In event-driven systems, circular event chains are a version of the same problem.

Shared kernel sprawl. A shared kernel grows to contain full business objects rather than just identifiers and value objects. Now every team is coupled to every field change. Keep the shared kernel minimal.

graph TD P[Problem: Circular Dependency] O[Order Context] -- "calls" --> B[Billing Context] B -- "calls back" --> O S[Solution: Extract concept] OA[Order Context] -- "emits OrderConfirmed" --> TX[Transaction Context] TX -- "emits PaymentInitiated" --> BA[Billing Context] BA -- "emits PaymentCaptured" --> OA

The circular dependency reveals a missing concept: Transaction Context owns the payment initiation flow that both Order and Billing were sharing between themselves.

Key Takeaways

  • A context map shows bounded contexts, teams, and the direction of influence — the most important information is who is upstream (has power) and who is downstream (absorbs change).
  • Embed context maps close to the code, in per-service CONTEXT.md files — far-away architecture diagrams get updated never.
  • Consumer-driven contract tests are the living version of the context map for event and API schemas — they fail when contracts break.
  • Fan-out from a single context is a warning sign: either it's a deliberate platform service, or it's a hidden monolith masquerading as microservices.
  • Circular dependencies between contexts reveal a missing bounded context — extract the shared concept rather than accepting the bidirectional coupling.
  • The shared kernel should contain only identifiers and primitive value objects — the moment it contains business entities, you've created a distributed monolith.
Share: