advanced 3 min answer

Two bounded contexts, Ordering and Fulfilment, share a library containing the OrderStatus enum so neither drifts. What happens over the next year, and what should have been shared instead?

bounded-contextsshared-kernelsemantic-couplingdeployment-orderddd
Show the full answer Hide the answer

Second by second, what happens

Month one. The library works. Both contexts compile against the same seven statuses and nobody has to remember a mapping.

Month four. Fulfilment needs AWAITING_CARRIER_PICKUP. It is meaningless to Ordering, which has no concept of carriers, but the enum lives in the shared library, so the value is added there. Ordering now compiles against a status it must ignore, and someone adds a comment saying so.

Month seven. Ordering splits CONFIRMED into CONFIRMED and PAYMENT_PENDING. This is a change to Ordering's own domain, and it cannot ship without a library release that Fulfilment must consume, because the enum is in Fulfilment's compile path. A change internal to one context has become a coordinated release. The teams start batching changes, and batching is where the release cadence goes: two teams that shipped daily now add about 10 days of lead time to anything touching status.

Month nine. Fulfilment's switch over the enum has a default branch written when there were seven values. It now silently treats PAYMENT_PENDING as shippable. Nothing errors. The mistake surfaces in a reconciliation days later.

Month twelve. The library has eleven statuses, three of which are meaningful to one context each, and a wiki page explaining which. The shared kernel has become the place where both domains' vocabularies are stored, which is the opposite of a bounded context.

Where it amplifies

The amplification is in the build graph, not the runtime. A shared type is build-time coupling, which converts every change into a lockstep deployment across teams that otherwise have nothing to coordinate. Then it amplifies again in semantics: once the types are identical, nobody writes the translation that would have forced the question "does CONFIRMED mean the same thing on both sides?" The answer, after a year, is no, and there is no layer where the difference is written down.

What should have been shared

The published language, not the type. Ordering owns its statuses and publishes them as part of its contract — in an event schema, an OpenAPI enum, a documented code list. Fulfilment maps that contract onto its own internal states at the boundary, in an anti-corruption layer it owns and can change alone.

  • Each context's enum is defined inside it, so adding a value is a one-team change.
  • The mapping is a real artefact with tests, so adding an unmapped status fails closed at the boundary rather than falling into a default branch.
  • The compatibility question becomes explicit: a new upstream status is a schema change with a deployment order, which is the conversation you want.

A shared kernel is legitimate — the idea is Evans's, from Domain-Driven Design (2003) — but choose to put only things with no domain meaning in it: money types, identifiers, time handling. The moment a shared type encodes a business state, it has taken ownership of a decision that belongs to one context.

What stops it

A fitness function on the dependency graph: no module may depend on another context's internal types, enforced in the build. And a review rule with teeth: a pull request adding a value to a shared enum must name every context that will compile against it. That is usually enough to make the shared kernel feel as expensive as it is.

When not to separate the types

If both contexts are owned by one team, deployed together, and the state machine genuinely is one concept, duplication is the worse choice and the compiler catching the missing case is a real benefit. The cost appears when the deployment units separate. Decide by ownership and release independence, not by how similar the two enums look.