Designing and Integrating Bounded Contexts
This is the third deep-dive in a series on Domain-Driven Design. It builds on the concepts introduced in Domain-Driven Design: A Practical Overview.
What a Bounded Context Actually Is
A bounded context is a boundary within which a particular model is defined and applicable. Inside that boundary, every term has exactly one meaning. Outside it, the same term might mean something different.
This sounds abstract until you see it in practice. In an e-commerce system, “Order” in the ordering context means a customer’s intent to purchase. It has line items, a shipping address, and pricing. “Order” in the fulfillment context means a set of items to pick, pack, and ship. It has warehouse locations, carrier selection, and tracking numbers. “Order” in the billing context means a charge to collect. It has payment methods, invoice lines, and tax calculations.
These are not three views of the same thing. They are three different models that serve three different purposes. A bounded context gives each one a home where it can evolve independently without corrupting the others.
Finding the Boundaries
Bounded context boundaries are discovered, not invented. They emerge from how the business works: how teams are organized, how language shifts, where responsibilities change hands.
Five Signals to Watch For
When exploring a domain, particularly during Event Storming, these signals help identify where boundaries belong.
Shared nouns. Events that cluster around the same domain concepts belong together. If a group of events all reference “Order,” “LineItem,” and “Customer,” they likely belong in the same context. When events start referencing “Shipment,” “Carrier,” and “Warehouse,” you’ve crossed into a different one.
Shared actors. Events triggered by the same roles tend to belong together. If the “Sales Rep” is the primary actor for a cluster of events, those events likely form a cohesive context. When the primary actor shifts to “Warehouse Manager,” you’re looking at a different context.
Temporal cohesion. Events that occur in close temporal sequence often belong together. The flow from “Order Placed” through “Payment Authorized” to “Order Confirmed” is a tight temporal cluster. “Shipment Dispatched” happens later, in a different phase of the process, likely in a different context.
Language boundaries. This is the strongest signal. When the same word means different things to different people, you’re at a context boundary. If the sales team says “customer” and means “someone who might buy” while the support team says “customer” and means “someone with an active subscription,” those are different bounded contexts with different models of what a customer is. See Ubiquitous Language and Naming Strategies for more on this.
Business capability. Each bounded context should align with a distinct business capability: ordering, fulfillment, billing, identity management. If a context is doing two unrelated things, it’s probably two contexts merged together. If two contexts are always changing for the same business reasons, they might be one context artificially split.
The Organizational Mirror
Bounded contexts often mirror organizational structure, and that’s not a flaw. It’s Conway’s Law working in your favor. The ordering team owns the ordering context. The fulfillment team owns the fulfillment context. Each team has autonomy over its own model and can evolve it independently.
Problems arise when bounded context boundaries don’t match team boundaries. If two teams co-own one context, coordination overhead increases. If one team owns three contexts, they’re spread thin. Aligning contexts with teams isn’t a hard rule, but misalignment is worth noticing.
Sizing a Bounded Context
There’s no formula. A bounded context should be large enough to be cohesive and small enough to be autonomous.
Too large: A context that contains multiple sets of business rules that change for different reasons. If changes to pricing logic force you to test and redeploy fulfillment logic, the context is too large.
Too small: A context so narrow that every operation requires calling three other contexts. If placing an order requires synchronous calls to inventory, pricing, and customer contexts before anything can happen, the boundaries are too granular.
Right-sized: A context that can handle its core operations without synchronous dependencies on other contexts. It owns its data, enforces its rules, and publishes events when something meaningful happens. Other contexts react to those events on their own terms and timeline.
Context Mapping: How Contexts Relate
Once you have boundaries, you need integration patterns. Context mapping is the practice of defining the relationships between bounded contexts: who depends on whom, and how they communicate.
Customer-Supplier
The most common pattern. One context (the supplier) produces something that another context (the customer) consumes. The supplier publishes events or exposes an API. The customer subscribes or calls in.
The key question is who has the power. In a healthy customer-supplier relationship, the customer can negotiate what the supplier provides. The supplier considers the customer’s needs when evolving its model.
Ordering (Supplier) → publishes OrderPlaced → Fulfillment (Customer)
Fulfillment depends on ordering, but ordering doesn’t know or care about fulfillment’s internal model. Ordering publishes what happened. Fulfillment decides what to do with it.
Anti-Corruption Layer (ACL)
When a downstream context needs to protect its model from an upstream context’s model, especially when the upstream is an external system or a legacy system you can’t change, an anti-corruption layer provides the translation.
The ACL sits at the boundary of the downstream context and converts the upstream’s language into the downstream’s language. The downstream model never references the upstream’s types directly.
External Payment Gateway → ACL → Billing Context
The payment gateway speaks in terms of “transactions,” “merchant IDs,” and “settlement batches.” The billing context speaks in terms of “payments,” “charges,” and “refunds.” The ACL translates between the two, keeping the billing model clean.
This is also the right pattern when integrating with a context whose model is poorly designed or unstable. Rather than conforming to a bad model, translate it at the boundary and keep your domain clean.
Conformist
Sometimes the upstream context has the power and won’t accommodate the downstream. The downstream accepts the upstream’s model as-is, without translation.
This is common when consuming industry-standard APIs or widely-used platforms. Fighting the model costs more than accepting it. The downstream context just uses the upstream’s types directly.
Use this pattern deliberately. It’s a conscious choice to accept a dependency on someone else’s model, not a failure to design your own.
Shared Kernel
Two contexts share a small, explicitly defined subset of the model. Both contexts use the same types, and changes to those types require coordination between both teams.
Ordering ←→ Shared Kernel (Money, Address) ←→ Fulfillment
The shared kernel should be small and stable. If it’s growing or changing frequently, it’s a sign that the contexts aren’t as separate as they should be. A NuGet package like DddSeedwork is an example of a shared kernel at the infrastructure level: common base types that multiple contexts use without coupling their domain models.
Published Language
A well-defined, documented schema that serves as the contract between contexts. Common in event-driven architectures where events are the integration mechanism.
The published language is versioned and stable. Consumers can rely on its structure. Producers can evolve their internal model freely as long as the published events remain compatible.
This is the pattern behind integration events. These are events that cross context boundaries, as opposed to domain events that stay internal.
Integration Patterns in Practice
Events Over Commands
Prefer asynchronous events over synchronous commands for cross-context communication. When the ordering context publishes OrderPlaced, it doesn’t tell fulfillment what to do. It reports what happened. Fulfillment decides independently how to react.
This keeps contexts autonomous. Ordering doesn’t need to know that fulfillment exists. If a new context like analytics wants to react to OrderPlaced, it subscribes without any changes to ordering.
Domain Events vs. Integration Events
Domain events stay inside a bounded context. They use the context’s internal language and types. OrderItem.QuantityUpdated is a domain event. It triggers internal recalculations within the ordering context.
Integration events cross context boundaries. They use the published language and carry only the information other contexts need. OrderPlaced with an OrderId, CustomerId, and TotalAmount is an integration event. It tells other contexts what happened without exposing ordering’s internal model.
Don’t publish your domain events directly as integration events. The internal model will change, and every downstream context will break. Translate domain events into integration events at the boundary.
Separate Data Stores
Each bounded context should own its data. If fulfillment needs customer information, it stores its own copy, the subset it needs, and keeps it in sync through events. It does not query the ordering context’s database directly.
This feels redundant until you consider the alternative: a shared database where every context’s migrations can break every other context, where query performance is coupled, and where ownership of data is ambiguous. Separate stores are the price of autonomy, and autonomy is what lets contexts evolve independently.
Common Mistakes
Drawing boundaries around technical layers instead of business capabilities. A “data access context” and a “business logic context” aren’t bounded contexts. They’re layers. Bounded contexts are vertical slices through the business: ordering, fulfillment, billing.
Sharing a database between contexts. This creates invisible coupling. One context’s schema change breaks another context’s queries. Own your data.
Making all communication synchronous. If placing an order requires a synchronous call to inventory, pricing, fulfillment, and billing before returning a response, you’ve built a distributed monolith. Use events. Let contexts react asynchronously.
Not mapping relationships explicitly. If you can’t draw a context map showing which contexts depend on which and what pattern they use, the relationships are implicit. That means they’re unmanaged. Draw the map. Name the patterns.
Splitting too early. Start with fewer, larger contexts and split when you have evidence of diverging language or diverging rates of change. It’s easier to split a context that’s too large than to merge contexts that were split prematurely.
Drawing the Map
A context map is a simple diagram showing your bounded contexts and the relationships between them. It doesn’t need to be formal. Boxes with labeled arrows on a whiteboard work fine.
For each relationship, note:
- The direction of dependency (who depends on whom)
- The integration pattern (customer-supplier, ACL, conformist, shared kernel)
- The communication mechanism (events, API calls, shared library)
Review the map when the system evolves. New contexts emerge. Relationships shift. A conformist relationship might need an ACL as the upstream’s model diverges from what the downstream needs. The map is a living document, not a one-time artifact.
Bounded contexts turn the ubiquitous language from a system-wide aspiration into a practical reality. Each context gets its own language, its own model, and its own autonomy. The next posts in this series explore what happens inside a bounded context: how to model aggregates and consistency boundaries, and how domain events flow within and between contexts.
Found this useful?
If this post helped you, consider buying me a coffee.
Comments