Bounded Context Mapping — DDD Context Maps for Microservices
What is a bounded context?#
A bounded context is a boundary within which a particular domain model is defined and applicable. Inside the boundary, every term has a precise, unambiguous meaning. Outside, the same word might mean something completely different.
Example: "Account" in the Banking context means a financial account with a balance. "Account" in the Identity context means a user login with credentials. Same word, different models, different bounded contexts.
Why context mapping matters#
When you build microservices, each service typically owns one bounded context. The hard problem is not what happens inside each context — it is what happens at the boundaries where contexts interact.
A context map is a visual and strategic tool that documents the relationships between bounded contexts. It answers: who depends on whom, who controls the shared model, and how do models translate at boundaries?
The seven relationship patterns#
1. Shared Kernel#
Two bounded contexts share a subset of the domain model. Both teams own the shared code and must coordinate changes.
┌─────────────┐ ┌─────────────┐
│ Shipping │ │ Billing │
│ Context │ │ Context │
│ ┌───┼─────┼───┐ │
│ │ Address │ │ │
│ │ Model │ │ │
│ └───┼─────┼───┘ │
└─────────────┘ └─────────────┘
When to use: when two contexts are closely related and maintained by the same team or tightly collaborating teams. The shared kernel is small and well-defined.
Risk: changes to the shared kernel require coordination. If the kernel grows too large, you have a distributed monolith.
2. Customer-Supplier#
One context (the supplier/upstream) provides data or services to another (the customer/downstream). The upstream team plans work with the downstream team's needs in mind.
The downstream team can influence the upstream API, but the upstream team ultimately decides what to build.
Example: the Orders context (upstream) supplies order data to the Analytics context (downstream). The Analytics team requests specific events or fields, and the Orders team accommodates within reason.
When to use: when there is a clear producer-consumer relationship and the upstream team is willing to collaborate.
3. Conformist#
Like customer-supplier, but the downstream team has no influence over the upstream. The downstream context conforms to whatever model the upstream provides.
Example: your service consumes a third-party payment API. You cannot change their data model. You conform to it.
When to use: when the upstream is an external system, a legacy system, or a team that will not accommodate your needs.
Risk: your domain model becomes polluted by upstream concepts that do not fit your domain language.
4. Anti-Corruption Layer (ACL)#
The downstream context builds a translation layer that converts the upstream model into its own domain language. The ACL isolates your domain from external influence.
┌──────────┐ ┌─────────────────┐ ┌──────────┐
│ External │ │ Anti-Corruption │ │ Your │
│ System │────▶│ Layer │────▶│ Context │
│ │ │ (translates) │ │ │
└──────────┘ └─────────────────┘ └──────────┘
The ACL contains adapters, translators, and facades that convert external data structures into your domain objects.
// Anti-corruption layer: translate external payment model to our domain
class PaymentACL {
translateFromStripe(stripeCharge: StripeCharge): Payment {
return {
id: generateId(),
amount: Money.fromCents(stripeCharge.amount),
currency: Currency.from(stripeCharge.currency),
status: this.mapStatus(stripeCharge.status),
paidAt: new Date(stripeCharge.created * 1000),
};
}
private mapStatus(stripeStatus: string): PaymentStatus {
const mapping: Record<string, PaymentStatus> = {
succeeded: PaymentStatus.COMPLETED,
pending: PaymentStatus.PROCESSING,
failed: PaymentStatus.FAILED,
};
return mapping[stripeStatus] || PaymentStatus.UNKNOWN;
}
}
When to use: always, when integrating with external systems or legacy systems whose model does not match yours. The ACL is one of the most important DDD patterns.
5. Open Host Service (OHS)#
The upstream context provides a well-defined, stable protocol (API) that any downstream context can consume. Instead of custom integrations for each consumer, the upstream publishes a general-purpose service.
Example: the User context exposes a REST API with versioned endpoints. Any context that needs user data consumes this API. No custom integrations per consumer.
When to use: when you have multiple downstream consumers and want to avoid per-consumer coupling.
6. Published Language#
A shared, well-documented data format used for communication between contexts. Often paired with Open Host Service.
Examples of published languages:
- Protocol Buffers (protobuf) schemas
- JSON Schema definitions
- Avro schemas in a schema registry
- OpenAPI specifications
- Domain events published to a message broker
// Published language: shared event schema
message OrderPlaced {
string order_id = 1;
string customer_id = 2;
repeated LineItem items = 3;
google.protobuf.Timestamp placed_at = 4;
}
When to use: when multiple contexts need to exchange data and you want a formal, versioned contract.
7. Separate Ways#
Two contexts have no integration at all. They operate independently. If there is overlap, each context implements its own version.
When to use: when the cost of integration outweighs the benefit. Sometimes duplication is cheaper than coupling.
Drawing a context map#
A context map is a diagram showing all bounded contexts and their relationships. Notation conventions:
- U = upstream, D = downstream
- ACL = anti-corruption layer
- OHS = open host service
- PL = published language
- SK = shared kernel
- CF = conformist
┌──────────────────┐ ┌──────────────────┐
│ Orders (U) │ │ Shipping (D) │
│ │──OHS/PL──▶│ [ACL] │
│ │ │ │
└──────────────────┘ └──────────────────┘
│
│ Customer-Supplier
▼
┌──────────────────┐ ┌──────────────────┐
│ Analytics (D) │ │ Payments (D) │
│ │ │ [ACL] │
└──────────────────┘ └──────────────────┘
▲
│ Conformist
┌───────┴──────────┐
│ Stripe (U) │
│ (external) │
└──────────────────┘
Context mapping in practice#
Step 1: Identify bounded contexts#
Talk to domain experts. Listen for language boundaries — where the same word means different things to different people, you have found a context boundary.
Step 2: Classify relationships#
For each pair of interacting contexts, determine: who is upstream? Who is downstream? Is there collaboration, or is one side conforming?
Step 3: Choose integration patterns#
Based on the relationship type and team dynamics:
- Same team, closely related? Shared kernel
- Collaborative teams, clear producer-consumer? Customer-supplier
- No control over upstream? Conformist + ACL
- Multiple consumers? Open host service + Published language
- Not worth integrating? Separate ways
Step 4: Implement translation#
At every boundary, implement the appropriate translation mechanism. ACLs for external systems. Shared schemas for published languages. API contracts for open host services.
Anti-patterns to avoid#
The Big Ball of Mud — no clear boundaries between contexts. Every service reaches into every other service's database. This is the absence of bounded contexts.
Shared database — two services reading and writing the same tables. This couples their schemas and deployment cycles. Each bounded context should own its data.
Leaking domain language — using upstream terminology in your domain model without translation. Your Shipping context should not use Stripe's charge object directly.
Over-integration — connecting every context to every other context. Sometimes separate ways is the right answer.
Context mapping and microservices#
The ideal microservice boundary aligns with a bounded context boundary. Each service:
- Owns its data store
- Exposes an API (open host service)
- Publishes events in a shared schema (published language)
- Translates external models at the boundary (ACL)
When you decompose a monolith, context maps tell you where to cut.
Visualize your context map#
Map your bounded contexts and their relationships with Codelit. Generate an interactive diagram showing upstream/downstream dependencies, ACLs, and shared kernels.
Key takeaways#
- Bounded contexts define where a domain model is valid — same word, different meaning, different context
- Context maps document relationships between contexts: who depends on whom and how
- Anti-corruption layers protect your domain from external model pollution
- Open host service + published language scales to multiple consumers
- Shared kernels require tight coordination — keep them small
- Conformist is the reality when integrating with external APIs
- Microservice boundaries should align with bounded context boundaries
- This is article #383 of our ongoing system design series
Try it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Related articles
Try these templates
Scalable SaaS Application
Modern SaaS with microservices, event-driven processing, and multi-tenant architecture.
10 componentsNetflix Video Streaming Architecture
Global video streaming platform with adaptive bitrate, CDN distribution, and recommendation engine.
10 componentsSearch Engine Architecture
Web-scale search with crawling, indexing, ranking, and sub-second query serving.
8 componentsBuild this architecture
Generate an interactive architecture for Bounded Context Mapping in seconds.
Try it in Codelit →
Comments