Loading content...
Once we've identified bounded contexts and drawn their boundaries, we face the next challenge: how do these contexts relate to each other? Real systems don't consist of isolated silos. Contexts must communicate, share information, and coordinate activities. The patterns they use to do this significantly impact system maintainability, team autonomy, and evolutionary flexibility.
Context Mapping is the DDD discipline of explicitly identifying and documenting the relationships between bounded contexts. It provides a vocabulary for discussing these relationships precisely, elevating discussions from vague generalities ('they integrate somehow') to specific, actionable patterns ('Sales is upstream of Fulfillment with an Open Host Service').
This page introduces the complete vocabulary of context relationships—the patterns that describe how bounded contexts influence, depend on, and integrate with each other.
By the end of this page, you will understand the full taxonomy of context relationship patterns, know when to apply each pattern, and be able to create comprehensive context maps for complex systems. You'll gain a precise vocabulary for discussing integration strategies.
A Context Map is a visual and conceptual representation of all bounded contexts in a system and the relationships between them. It serves as a strategic document that:
The context map isn't just documentation—it's a strategic thinking tool. Creating one forces you to make implicit assumptions explicit.
123456789101112131415161718192021222324252627282930313233343536
Context Map Elements═════════════════════════════════════════════════════════════════════════════════ 1. BOUNDED CONTEXTS (Boxes) ┌─────────────────────────┐ │ CONTEXT NAME │ │ ──────────── │ │ Team: TeamName │ ← Ownership │ Type: Core/Support │ ← Strategic classification └─────────────────────────┘ 2. RELATIONSHIPS (Lines/Arrows) Context A ──────→ Context B Direction indicates influence/dependency Context A ←─ACL── Context B Pattern label on the line Context A ═══════ Context B Partnership (equal influence) 3. RELATIONSHIP PATTERNS (Labels) Pattern Symbol Description ─────────────────────────────────────────────────────────────────────── Partnership P Mutual cooperation Shared Kernel SK Shared model subset Customer-Supplier C/S Downstream influences upstream Conformist CF Downstream accepts upstream model Anti-Corruption Layer ACL Translation layer protects context Open Host Service OHS Published API for multiple consumers Published Language PL Well-documented shared format Separate Ways SW No integration (deliberate) Big Ball of Mud BBoM No clear boundaries (reality) 4. EXTERNAL SYSTEMS ╔═════════════════════════╗ ║ EXTERNAL SYSTEM ║ Double border indicates ║ (Third Party) ║ external/legacy system ╚═════════════════════════╝Context maps should be living documents. As the system evolves, the map must evolve with it. Many teams display the context map prominently (physical or virtual wall) and update it as part of architectural reviews.
Partnership describes a relationship where two contexts have a mutual dependency and the teams coordinate closely to meet shared objectives. Neither team dominates; both have roughly equal influence over the integration.
| Use When | Avoid When |
|---|---|
| Both teams are in the same organization and can coordinate easily | Teams are in different organizations with different priorities |
| Features frequently require changes to both contexts simultaneously | Contexts have independent release cycles and change for different reasons |
| Teams have compatible goals and incentives | Teams have conflicting priorities or compete for resources |
| The integration is core to both contexts' value proposition | Integration is peripheral to one or both contexts |
Partnership has high coordination overhead. It works only when both teams truly benefit from close collaboration. If one team is always accommodating the other, it's not partnership—it's an unacknowledged upstream/downstream relationship that should be formalized.
12345678910111213141516171819202122232425
E-Commerce Platform: Shopping Cart + Pricing Partnership═════════════════════════════════════════════════════════════════════════════════ ┌─────────────────────┐ ┌─────────────────────┐ │ SHOPPING CART │ Partnership │ PRICING │ │ ───────────── │ ══════════════ │ ──────── │ │ Team: Cart Eng │ │ Team: Pricing Eng │ │ Owns: Cart logic │ │ Owns: Prices │ └─────────────────────┘ └─────────────────────┘ │ │ └──────────────────┬───────────────────┘ │ Joint Planning Sessions Every Sprint ───────────────────────────────────── • Cart needs real-time price updates • Pricing needs cart context for discounts • Both teams align on API changes • Synchronized releases for promotions Why Partnership Works Here:• Both contexts are core to checkout experience• Price calculation requires cart content• Cart display requires current prices• Changes are frequent and bidirectional• Teams share the goal: maximize conversionShared Kernel is a pattern where two contexts share a small subset of domain model code that is jointly owned. Both teams can modify this shared code, and any changes must be coordinated.
The shared kernel typically includes:
Importantly, the shared kernel is as small as possible. It represents the absolute minimum that genuinely needs to be shared, not a dumping ground for convenience.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// ═══════════════════════════════════════════════════════════════════════════// SHARED KERNEL: Minimal types shared across bounded contexts// Package: @company/shared-kernel// ═══════════════════════════════════════════════════════════════════════════ // ─────────────────────────────────────────────────────────────────────────────// VALUE OBJECTS: Fundamental types with no business logic// ───────────────────────────────────────────────────────────────────────────── export class CustomerId { private constructor(private readonly value: string) { if (!value || value.length === 0) { throw new Error("CustomerId cannot be empty"); } } static create(value: string): CustomerId { return new CustomerId(value); } static generate(): CustomerId { return new CustomerId(crypto.randomUUID()); } equals(other: CustomerId): boolean { return this.value === other.value; } toString(): string { return this.value; }} export class Money { private constructor( private readonly amount: number, private readonly currency: Currency ) { if (amount < 0) { throw new Error("Money amount cannot be negative"); } } static of(amount: number, currency: Currency): Money { return new Money(amount, currency); } add(other: Money): Money { if (!this.currency.equals(other.currency)) { throw new Error("Cannot add money of different currencies"); } return new Money(this.amount + other.amount, this.currency); } // Other money operations...} export type Currency = 'USD' | 'EUR' | 'GBP' | 'JPY'; // ─────────────────────────────────────────────────────────────────────────────// INTEGRATION EVENTS: Events that cross context boundaries// ───────────────────────────────────────────────────────────────────────────── export interface IntegrationEvent { readonly eventId: string; readonly occurredAt: Date; readonly eventType: string;} export interface CustomerRegistered extends IntegrationEvent { eventType: 'CustomerRegistered'; customerId: string; // Note: string, not CustomerId (for serialization) email: string; registeredAt: Date;} export interface OrderPlaced extends IntegrationEvent { eventType: 'OrderPlaced'; orderId: string; customerId: string; totalAmount: number; currency: Currency; placedAt: Date;} // ─────────────────────────────────────────────────────────────────────────────// GOVERNANCE: Rules for shared kernel modification// ───────────────────────────────────────────────────────────────────────────── /** * SHARED KERNEL GOVERNANCE RULES: * * 1. Any change requires approval from ALL teams using this kernel * 2. Changes must be backward compatible OR coordinated release * 3. No business logic—only fundamental types and events * 4. Keep it minimal—if in doubt, don't share * 5. Semantic versioning with major bumps for breaking changes * 6. Comprehensive test coverage required */Shared kernels create tight coupling. Every change requires multi-team coordination. If the shared kernel grows too large, it becomes a distributed monolith hidden inside a package. Use sparingly and keep it minimal. When in doubt, duplicate instead of share.
Customer-Supplier (also called Upstream-Downstream) describes a relationship where one context (upstream/supplier) provides data or services that another context (downstream/customer) consumes. The key distinction from other patterns is that the downstream team has influence over the upstream team's priorities.
In a customer-supplier relationship:
12345678910111213141516171819202122232425262728
Order Processing: Sales (Upstream) → Fulfillment (Downstream)═════════════════════════════════════════════════════════════════════════════════ ┌─────────────────────┐ ┌─────────────────────┐ │ SALES │ Customer/ │ FULFILLMENT │ │ (Upstream) │ Supplier │ (Downstream) │ │ ───────────── │ ──────────────→│ ──────────── │ │ Produces: │ │ Consumes: │ │ • OrderPlaced │ │ • OrderPlaced │ │ • OrderModified │ │ │ │ • OrderCancelled │ │ Needs: │ └─────────────────────┘ │ • Shipping address│ │ • Line items │ │ • Priority level │ └─────────────────────┘ Customer-Supplier Agreement:────────────────────────────────────────────────────────────────────────────• Fulfillment can request new fields in OrderPlaced event• Sales commits to including requested fields if reasonable• Sales notifies Fulfillment before any breaking changes• Both teams review integration needs quarterly This differs from Conformist because:────────────────────────────────────────────────────────────────────────────• Fulfillment CAN request changes (vs. must accept what's given)• Sales WILL consider Fulfillment's needs (vs. ignoring downstream)• There IS a relationship and communication (vs. one-way consumption)The key to successful customer-supplier relationships is explicit negotiation. The downstream team should articulate their needs clearly. The upstream team should consider those needs genuinely. Regular sync meetings (monthly or quarterly) keep the relationship healthy. Without explicit communication, customer-supplier degrades into conformist.
Conformist describes a relationship where the downstream context accepts the upstream context's model as-is, without any translation or negotiation. The downstream team 'conforms' to whatever the upstream provides.
This pattern typically occurs when:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// ═══════════════════════════════════════════════════════════════════════════// CONFORMIST EXAMPLE: Our Billing context conforms to Stripe's model// ═══════════════════════════════════════════════════════════════════════════ // We use Stripe's types directly in our domain layer// This is conformist—we accept their model without translation import Stripe from 'stripe'; // Our domain service uses Stripe's types directlyexport class PaymentService { constructor(private stripe: Stripe) {} async processPayment( amount: number, currency: string, // Using Stripe's payment method type directly paymentMethod: Stripe.PaymentMethod, // Using Stripe's customer type directly customer: Stripe.Customer ): Promise<Stripe.PaymentIntent> { // We conform to Stripe's API structure const paymentIntent = await this.stripe.paymentIntents.create({ amount, currency, customer: customer.id, payment_method: paymentMethod.id, confirm: true, }); // We return Stripe's type, not our own return paymentIntent; }} // Our entities/aggregates reference Stripe concepts directlyinterface OrderPayment { orderId: string; // Stripe's ID, not our own concept stripePaymentIntentId: string; // Stripe's status enum, not our own status: Stripe.PaymentIntent.Status;} // ═══════════════════════════════════════════════════════════════════════════// WHY CONFORMIST HERE?// ═══════════════════════════════════════════════════════════════════════════//// 1. Stripe is a third-party—we cannot influence their model// 2. Stripe's payment model is well-designed and widely understood// 3. Translation would add complexity with little benefit// 4. Our payment subdomain is not core—it's supporting//// RISKS ACCEPTED:// - Stripe model changes force our code changes// - "PaymentIntent" terminology leaks into our domain language// - Tightly coupled to Stripe's SDK//// ═══════════════════════════════════════════════════════════════════════════Conformist isn't always bad. When the upstream model is well-designed and stable (like major payment gateways), translation adds overhead without benefit. For non-core subdomains where the upstream model is 'good enough', conformist is pragmatic. Reserve translation efforts (ACL) for core domain protection.
The Anti-Corruption Layer (ACL) is a translation mechanism that protects a bounded context from the conceptual model of another context (especially legacy systems or external systems). The ACL translates between models, ensuring the downstream context uses its own ubiquitous language without pollution from upstream concepts.
The ACL is the opposite of conformist: instead of accepting the foreign model, we actively translate it.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
// ═══════════════════════════════════════════════════════════════════════════// ANTI-CORRUPTION LAYER: Protecting our domain from legacy system// ═══════════════════════════════════════════════════════════════════════════ // ─────────────────────────────────────────────────────────────────────────────// EXTERNAL SYSTEM (Legacy ERP we integrate with)// ───────────────────────────────────────────────────────────────────────────── // Legacy system's types (ugly, inconsistent naming, wrong concepts)namespace LegacyERP { export interface CUST_MSTR { CUST_ID: string; CUST_NM: string; CUST_ADDR1: string; CUST_ADDR2: string; CUST_CITY: string; CUST_ST: string; CUST_ZIP: string; CUST_TEL: string; CUST_TAX_EXMPT: 'Y' | 'N' | null; CR_LMT: number; ACT_BAL: number; LST_ORD_DT: string; // Date as string: "20240115" }} // ─────────────────────────────────────────────────────────────────────────────// OUR DOMAIN MODEL (Clean, expressive, follows ubiquitous language)// ───────────────────────────────────────────────────────────────────────────── namespace CustomerDomain { export interface Customer { readonly id: CustomerId; readonly name: string; readonly address: Address; readonly phone: PhoneNumber; readonly taxStatus: TaxStatus; readonly creditProfile: CreditProfile; readonly lastOrderDate: Date | null; } export interface Address { readonly street1: string; readonly street2: string | null; readonly city: string; readonly state: string; readonly postalCode: string; } export type TaxStatus = 'TAXABLE' | 'EXEMPT' | 'UNKNOWN'; export interface CreditProfile { readonly limit: Money; readonly currentBalance: Money; readonly availableCredit: Money; }} // ─────────────────────────────────────────────────────────────────────────────// ANTI-CORRUPTION LAYER// ───────────────────────────────────────────────────────────────────────────── class CustomerAntiCorruptionLayer { /** * Translates legacy ERP customer to our domain model */ translateFromLegacy(legacy: LegacyERP.CUST_MSTR): CustomerDomain.Customer { return { id: CustomerId.create(legacy.CUST_ID), name: this.normalizeName(legacy.CUST_NM), address: this.translateAddress(legacy), phone: PhoneNumber.parse(legacy.CUST_TEL), taxStatus: this.translateTaxStatus(legacy.CUST_TAX_EXMPT), creditProfile: this.translateCreditProfile(legacy), lastOrderDate: this.parseDate(legacy.LST_ORD_DT), }; } /** * Translates our domain model back to legacy format (for updates) */ translateToLegacy(customer: CustomerDomain.Customer): LegacyERP.CUST_MSTR { return { CUST_ID: customer.id.toString(), CUST_NM: customer.name.toUpperCase(), // Legacy wants uppercase CUST_ADDR1: customer.address.street1, CUST_ADDR2: customer.address.street2 || '', CUST_CITY: customer.address.city, CUST_ST: customer.address.state, CUST_ZIP: customer.address.postalCode, CUST_TEL: customer.phone.toString(), CUST_TAX_EXMPT: this.translateTaxStatusToLegacy(customer.taxStatus), CR_LMT: customer.creditProfile.limit.getAmount(), ACT_BAL: customer.creditProfile.currentBalance.getAmount(), LST_ORD_DT: this.formatDateForLegacy(customer.lastOrderDate), }; } private translateAddress(legacy: LegacyERP.CUST_MSTR): CustomerDomain.Address { return { street1: legacy.CUST_ADDR1.trim(), street2: legacy.CUST_ADDR2?.trim() || null, city: legacy.CUST_CITY.trim(), state: legacy.CUST_ST.trim(), postalCode: legacy.CUST_ZIP.trim(), }; } private translateTaxStatus(flag: 'Y' | 'N' | null): CustomerDomain.TaxStatus { switch (flag) { case 'Y': return 'EXEMPT'; case 'N': return 'TAXABLE'; default: return 'UNKNOWN'; } } private translateCreditProfile(legacy: LegacyERP.CUST_MSTR): CustomerDomain.CreditProfile { const limit = Money.of(legacy.CR_LMT, 'USD'); const balance = Money.of(legacy.ACT_BAL, 'USD'); return { limit, currentBalance: balance, availableCredit: limit.subtract(balance), }; } private parseDate(dateStr: string): Date | null { if (!dateStr || dateStr === '00000000') return null; // Parse "20240115" format const year = parseInt(dateStr.substring(0, 4)); const month = parseInt(dateStr.substring(4, 6)) - 1; const day = parseInt(dateStr.substring(6, 8)); return new Date(year, month, day); }} // ─────────────────────────────────────────────────────────────────────────────// USAGE: Repository uses ACL to fetch/save customers// ───────────────────────────────────────────────────────────────────────────── class CustomerRepository { constructor( private legacyClient: LegacyERPClient, private acl: CustomerAntiCorruptionLayer ) {} async findById(id: CustomerId): Promise<CustomerDomain.Customer | null> { // Fetch from legacy system const legacyData = await this.legacyClient.getCUST_MSTR(id.toString()); if (!legacyData) return null; // Translate through ACL return this.acl.translateFromLegacy(legacyData); } async save(customer: CustomerDomain.Customer): Promise<void> { // Translate to legacy format const legacyData = this.acl.translateToLegacy(customer); // Save to legacy system await this.legacyClient.updateCUST_MSTR(legacyData); }}Invest in ACLs when protecting your core domain. The translation cost pays off by preserving your ubiquitous language and shielding your domain from external instability. For supporting or generic subdomains, conformist may be more cost-effective.
Open Host Service (OHS) describes an upstream context that provides a well-defined, stable API for multiple downstream consumers. Rather than point-to-point integrations, the upstream publishes a general-purpose interface that any consumer can use.
Published Language (PL) often accompanies OHS—it's a well-documented, shared format (schema, protocol) that consumers use to interact with the service. Together, OHS and PL enable scalable, decoupled integration.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
Open Host Service: Product Catalog API═════════════════════════════════════════════════════════════════════════════════ ┌────────────────────────────────────────────┐ │ PRODUCT CATALOG CONTEXT │ │ (Open Host Service) │ │ │ │ Provides: │ │ • REST API: /api/v1/products │ │ • GraphQL: /graphql │ │ • Events: ProductCreated, ProductUpdated │ │ │ │ Published Language: │ │ • OpenAPI 3.0 specification │ │ • AsyncAPI event schemas │ │ • JSON Schema for all types │ └────────────────────────────────────────────┘ │ ┌────────────────────┴────────────────────┐ │ │ ▼ ▼ ┌─────────────────────────┐ ┌─────────────────────────┐ │ SEARCH CONTEXT │ │ RECOMMENDATIONS │ │ (Downstream) │ │ (Downstream) │ │ │ │ │ │ Consumes: │ │ Consumes: │ │ • ProductCreated │ │ • ProductUpdated │ │ • ProductUpdated │ │ • GET /products/{id} │ │ • ProductDeleted │ │ │ │ │ │ Uses Published │ │ Uses Published │ │ Language: JSON Schema │ │ Language: AsyncAPI │ │ │ └─────────────────────────┘ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ ANALYTICS CONTEXT │ │ (Downstream) │ │ │ │ Consumes: │ │ • All product events │ │ • GET /products │ │ │ │ Uses Published │ │ Language: Protobuf │ └─────────────────────────┘ Benefits:─────────────────────────────────────────────────────────────────────────────────• Product Catalog doesn't need to know its consumers• New consumers can self-onboard using documentation• Schema changes go through semantic versioning• Multiple protocol options (REST, GraphQL, events)• Published language enables code generationSeparate Ways is the explicit decision to NOT integrate two bounded contexts. The contexts remain completely independent, solving similar problems separately, even if this means duplicating effort.
This might seem wasteful, but separate ways is sometimes the right choice:
Separate ways means accepting duplication. The same business concept is modeled and maintained in multiple places. This is acceptable when integration cost exceeds duplication cost, but recognize that parallel implementations can drift and create inconsistencies.
We've covered the complete vocabulary for describing relationships between bounded contexts. Here's a concise reference:
| Pattern | Power Dynamic | Translation | Coordination |
|---|---|---|---|
| Partnership | Equal | Shared | High |
| Shared Kernel | Equal | None (shared code) | High |
| Customer-Supplier | Negotiated | Varied | Medium |
| Conformist | Upstream dominates | None (adopt) | Low |
| Anti-Corruption Layer | Downstream protects | Full translation | Low |
| Open Host Service | Upstream serves all | Published | Low |
| Separate Ways | None | None | None |
What's Next:
With context mapping patterns understood, we need to explore how contexts actually communicate data and coordinate activities. The next page covers Integration Between Contexts—the technical patterns for implementing the relationships we've discussed.
You now have a complete vocabulary for describing bounded context relationships. Next, we'll dive into the practical implementation patterns for making these relationships work in production systems.