Integrating Bounded Contexts
How Data and Events Flow Across Context Boundaries
Bounded contexts keep models clean and independent. But at runtime they still need to work together. Data crosses their boundaries, and one context often needs to react to something that happened in another. How those connections are made is its own design problem.
The choice of how two contexts communicate has consequences beyond the immediate connection. It shapes how tightly they are coupled, how failures propagate, and how much of the system has to move when one part changes.
The previous articles introduced bounded contexts and context mapping, using an online learning platform as the running example. On that platform, the billing context needs to react when a student enrolls, and the certification context needs to react when a course is completed. This article works through three decisions that determine how those connections should be made.
Direct Calls or Events
The first question: does the caller need a response, or is it notifying others that something happened?
A direct call is appropriate when the result is needed as part of the current operation. On the learning platform, the enrollment context needs invoice confirmation before it can consider the enrollment complete. It calls the billing context and waits for a response. Customer/supplier relationships often take this form when the downstream requires data from the upstream before it can proceed.
Events are appropriate when other contexts need to know something happened, but the originating context does not care what they do with that information or when. When a course is completed, the learning context publishes a CourseCompleted event. The certification context reacts by issuing a certificate. The learning context is entirely unaware of the certification context. Open host service relationships with multiple consumers are natural candidates for events with a published schema, since the upstream can notify all interested contexts without knowing who they are.
The distinction comes down to whether the downstream reaction is a prerequisite or a consequence. Invoice confirmation is a prerequisite for enrollment. The outcome matters to the current operation, and enrollment cannot proceed without it. A certificate is a consequence of course completion. It should happen, but the course is complete regardless of when or whether certification reacts.
When the outcome must be known before the current operation can continue, a direct call fits. When the originating context can declare that something happened and move on, events fit better.
Synchronous or Asynchronous
The second question: does the caller need to wait for the result, or can processing happen later?
Synchronous means the caller blocks until the callee responds. The flow is linear, errors are immediate, and the result is available right away. Direct calls are almost always synchronous. The tradeoffs are real. The caller fails if the callee is unavailable, and a slow callee makes a slow caller.
Asynchronous means the caller continues immediately and the callee processes in its own time. This is the natural mode for events: the learning context publishes CourseCompleted and moves on. The certification context processes it whenever it is ready. This introduces temporal decoupling, but also eventual consistency: the downstream reaction is not guaranteed to have happened at any given moment. A student might complete a course and not see their certificate immediately.
The failure modes differ in the same way. An async billing integration means enrollment can complete while the invoice silently fails to generate. The student is enrolled, the charge never happens, and nothing immediately signals that something went wrong. A synchronous certification integration means course completion blocks until the certification service responds. A slow or unavailable certification system makes every course completion slow.
The two decisions interact. Direct calls are typically synchronous and events are typically asynchronous. There are exceptions: a fire-and-forget direct call can be asynchronous, and an in-process event dispatcher can be synchronous. But for most integrations, choosing between direct calls and events largely determines the synchrony as well.
In-Process or Across Services
The third question: are the contexts in the same process or in separate services?
This question is often skipped because many articles about bounded context integration assume a distributed system from the start. But bounded contexts do not require separate services. In a modular monolith, all contexts live in the same process.
In a modular monolith, direct calls are method calls through a well-defined public interface. Events can be dispatched in-process through an event dispatcher, either synchronously within the same thread or asynchronously via a background worker or task queue. No network, no broker, no serialization overhead.
In a distributed system, direct calls become network calls: HTTP or gRPC. Asynchronous events require a message broker. Both add latency, partial failure modes, and operational complexity that in-process communication does not have.
On the learning platform, the enrollment context calling the billing context is a method call in a modular monolith. Moving to separate services turns that method call into an HTTP request. The logical relationship does not change. Enrollment still needs invoice confirmation before proceeding. What changes is that the call now crosses a network, which means it can time out, the billing service can be unavailable, and the caller needs to handle those cases explicitly.
The same transition applies to events: the CourseCompleted event dispatched in-process through an event dispatcher becomes a message published to a broker when the learning and certification contexts move to separate services.
Many teams start with a modular monolith and extract services later as the need arises. Designing for events with stable integration contracts from the start makes that transition smoother. The integration event contract does not change when the contexts move to separate processes, only the delivery mechanism does.
Domain Events vs. Integration Events
Deciding to use events does not settle what those events should look like. A domain event is internal to a context. It captures what happened in the language of that context and may carry details that only make sense within its own boundary. An integration event is a deliberately designed contract for external consumption. It is stable, versioned, and exposes only what downstream contexts actually need.
On the learning platform, the learning context might raise a rich CourseCompleted domain event internally, carrying identifiers and state that reflect its own model. The integration event published to other contexts would be leaner: a CourseCompletedEvent with the student identifier, the course identifier, and the completion timestamp. Nothing more.
Publishing internal domain events directly as integration events couples consumers to the internal model. When the domain model evolves, the integration contract breaks. Keeping the two separate gives each context the freedom to evolve internally without forcing changes on its consumers.
There is one more practical concern. Publishing an integration event and updating domain state are two separate operations that can fail independently. If the domain state is saved but the event is never published, subscribing contexts will never know the change happened.
The transactional outbox pattern addresses this by writing the event in the same transaction as the state change and letting a separate process handle publishing. This applies whether the contexts are in-process or across services.
Putting It Together
Integration decisions are not implementation details to sort out after the design is done. They determine what breaks when one context is slow, what stays isolated when one changes, and how much of the system needs to move together when something goes wrong. The context map pattern is the starting point. It tells you the nature of the relationship and narrows the options.
If the caller needs an immediate result as part of the current operation, use a direct call. If it is notifying other contexts that something happened and does not need to know what they do with it, use an event.
Direct calls are typically synchronous. Events are typically asynchronous. If the downstream reaction must be part of the same operation, keep it synchronous. If it can happen later, let it be asynchronous and accept eventual consistency.
Start with in-process communication in a modular monolith. Cross-process communication adds real cost: network latency, partial failures, serialization, and operational overhead. Extract to separate services when there is a concrete reason to do so, not by default.
A direct synchronous in-process method call is the simplest possible integration and a reasonable place to start. An asynchronous integration event delivered via a message broker is the most decoupled but also the most complex. Most integrations fall somewhere in between, and the three decisions help locate exactly where.
What Comes Next
Bounded contexts establish where models live. Context mapping names the relationships between them. Integration patterns determine how they communicate at runtime. What none of this addresses is how the inside of a single bounded context should be organized.
The billing context processes invoices and applies pricing rules. The learning context tracks course progression, evaluates completion criteria, and enforces the rules that make the platform worth using. Both have clear boundaries and clear connections. But should they be structured the same way internally?
The next article covers how to match the architecture of a bounded context to the nature of the subdomain it implements.
This article is part of the Domain-Driven Design series
- 1. What Is Domain-Driven Design?
- 2. Subdomains and Bounded Contexts in Domain-Driven Design
- 3. Context Mapping in Domain-Driven Design
- 4. Integrating Bounded Contexts