Skip to content
Knowledge Hub

Bounded context: both are "Order", but checkout and shipping do not need a shared model Intermediate

Estimated reading time: 8 min read

One Order class is shared from checkout all the way to fulfillment. It starts with total, paid, buyerId. Shipping adds recipientId, packageWeight, carrierStatus; checkout adds discounts and payment state. A change on the fulfillment side breaks the checkout build, and the two teams have to deploy together.

The cause is not that the name Order is wrong. The same name refers to two different business viewpoints: checkout sees an order as a transaction whose price and payment must be confirmed; shipping sees it as a request to pack and deliver.

A bounded context is the boundary within which one model and one language have a consistent meaning.

Subdomain and bounded context are not the same thing

Section titled “Subdomain and bounded context are not the same thing”
Subdomain Bounded context
Belongs to Problem space: the real business domain Solution space: the model and code you choose to build
Who decides The business (what already exists) The design team (what you choose)
Classification Core, supporting, generic No classification; one subdomain may contain several contexts
Example Shipping, invoicing, user accounts The ShippingJob model and its language

According to Microsoft Learn, the three subdomain types decide how much to invest:

  • Core: creates competitive advantage, deserves the most careful modeling.
  • Supporting: needed for operations but not a differentiator (for example invoicing).
  • Generic: a problem the industry has already solved; use an existing solution (for example user accounts).

A common goal is one bounded context per subdomain. Reality often deviates, which is fine as long as you know where you deviate.

Each context has its own ubiquitous language, used consistently in conversations, documents and code. The same word can mean different things in two contexts; that is the signal of a boundary.

Question Checkout Shipping
What does “Order” decide? Price, payment, purchase confirmation Preparation and delivery
Main data Buyer, amount, payment state Recipient, package, carrier state
Status paid ready-to-pack
Who owns the rules Checkout Shipping

Context map: how contexts talk to each other

Section titled “Context map: how contexts talk to each other”

Boundaries are not isolated. A context map draws the relationships between contexts; each relationship follows a pattern from Evans (DDD) and later work.

Loading diagram…

Pattern Meaning Use when
Partnership The two teams succeed or fail together and coordinate releases Genuine two-way dependency, teams work closely
Shared Kernel A small part of the model is shared; changes need both sides to agree Very small, stable part with governance; avoid at large scale
Customer-Supplier Upstream supplies, downstream has a say in upstream’s planning Two teams in the same organization, downstream needs to negotiate requirements
Conformist Downstream accepts the upstream model without negotiation Upstream is external or too dominant; translation costs more than it gains
Anti-corruption layer (ACL) Downstream builds a translation layer so upstream does not pollute its model Upstream is legacy or its model is very different; see Adapter pattern
Open Host Service (OHS) Upstream publishes a clear, versioned API/protocol for many consumers Many downstreams use it
Published Language A shared, documented data format (for example OpenAPI, an event schema) Goes together with OHS
Separate Ways No integration Integration costs more than it is worth

These sit on two different sides of the same seam:

  • Published Language (and OHS) is the upstream side: “this is the contract I publish”. In the demo it is OrderPaidEvent.
  • ACL is the downstream side: “I translate that contract into my model”. In the demo it is the code that creates a ShippingJob from the event.

The published event is not an ACL. The ACL is the receiver’s code. According to Microsoft Learn, an ACL should only translate; do not put business rules or orchestration in it.

  1. Event storming or whiteboard sketching with domain experts: list business events and who cares about them.
  2. Look for language fractures: places where one word has two meanings, or two groups use two words for one thing.
  3. Group tightly coupled functions; separate core from supporting and generic.
  4. Test against real changes: does a common request fit cleanly inside one context?
  5. Align with teams: each context should have one clear owning team. Per Microsoft, if one team must own many unrelated contexts, or one context needs many teams to coordinate, revisit the boundary or the team structure (Conway’s law).

According to Fowler, organizational culture is often the dominant factor when drawing boundaries, because different languages require different models.

How it works: Checkout publishes, Shipping translates

Section titled “How it works: Checkout publishes, Shipping translates”

Loading diagram…

  • Checkout publishes OrderPaid only when the order is paid according to Checkout’s rules. OrderPaidEvent carries orderId, recipientId, occurredAt, and not the full CheckoutOrder or an ORM object.
  • Shipping creates a ShippingJob with status pending. It knows nothing about totalMinor or discounts.
  • buyerId and recipientId are two different roles. If both are string, TypeScript cannot catch a mix-up, so field names and tests must carry the semantics.
  • An event does not grant access to data by itself. If Shipping needs the full address, design a separate contract or API, accounting for PII and versioning.

Event or synchronous API? An event fits when Shipping can tolerate late processing; a synchronous API fits when Checkout needs an answer immediately, but it ties both sides to shared availability.

With events, Shipping must be idempotent on the event ID so that broker retries do not create two jobs. Publishing an event in the same transaction as the data requires a transactional outbox.

Terminal window
npx --prefix examples ts-node examples/src/bounded-contexts/context-demo.ts

Actual output:

published: {
orderId: 'order-42',
recipientId: 'user-9',
occurredAt: '2026-10-01T00:00:00Z'
}
shipping: { orderId: 'order-42', recipientId: 'user-9', status: 'pending' }

The buyer is user-7 and the recipient is user-9; the event carries only the recipient. ShippingJob has no totalMinor. Remove recipientId from the event and the compiler fails where ShippingJob is created: contract changes must be deliberate. The demo runs in one process, with no database or broker.

  • Split when the same concept is pulled in two directions by two teams, or a change on one side breaks the other.
  • Do not split too early while the domain is still being learned: wrong boundaries force every workflow to make cross-calls.
  • Merge back if two contexts constantly ask each other about everything; the initial diagram is not worth defending over evidence.

Per-context models

  • Each part keeps its own terminology, invariants and pace of change.
  • Small contracts reduce the impact when an internal model changes.
  • Clear boundaries let you weigh splitting a service based on coupling evidence.

Costs and risks

  • Requires mapping, versioning and contract tests; some data looks duplicated.
  • Events or APIs across boundaries add failure modes, retries and consistency lag.
  • Splitting wrong causes too many cross-calls in workflows.

Checklist for reviewing a contract between two contexts

Section titled “Checklist for reviewing a contract between two contexts”
  1. The event or API carries only the fields downstream needs (such as orderId, recipientId, occurredAt), never a raw ORM entity or internal model.
  2. The contract is versioned and documented (Published Language); changing it is deliberate.
  3. Downstream has an ACL that translates into its own model; the ACL only translates and holds no business rules or orchestration.
  4. Fields with different roles have different names (buyerId vs recipientId) and tests that protect their meaning.
  5. Consumers are idempotent on the event ID; events are published in the same transaction as the data via an outbox.
  6. Sensitive data (addresses, PII) goes through a separate, versioned contract, not “conveniently” stuffed into the event.

Self-check:

  1. Why should Shipping not import the full CheckoutOrder?
  2. Is an external payment gateway usually Conformist or ACL, and which condition decides?
  3. Two teams keep editing the same context: should you revisit the boundary or the team structure?