Oh My Learning

Bounded Contexts As Autonomous Service Boundaries

implementing-ddd/03 drafted · 8 min read

Bounded contexts as autonomous service boundaries#

TL;DR#

A bounded context is the boundary within which a single model and a single ubiquitous language stay consistent; Vernon's operational rule is that this boundary should also be an autonomy boundary — one team, one codebase (or deployable unit), one database schema, one release cadence — because a linguistic/model boundary that doesn't line up with team and deployment boundaries erodes back into a shared, tangled model in practice.

The idea#

Evans defines a bounded context as the explicit boundary within which a domain model applies — inside it, a term like "Order" has one precise meaning; outside it, in a different bounded context, "Order" might mean something else entirely (a sales order vs. a fulfillment order vs. a purchase order) (see ddd-evans). That's a linguistic and conceptual definition. Vernon's contribution is to insist this isn't just a modeling nicety — it has to be enforced structurally, or it doesn't hold. If two teams share a database schema, a shared library of "common" domain classes, or a single deployable, the linguistic boundary will erode under delivery pressure: someone will "just add a field" to the shared Order class for their own bounded context's purpose, and now the model is polluted for everyone. This is why bounded contexts, in Vernon's treatment, map so closely onto what the microservices literature later called "service boundaries" — the same forces (team autonomy, independent deployability, no shared mutable state) apply to both, because a bounded context that isn't also an autonomous unit of software delivery is a bounded context in name only.

How it works#

Rule 1 — One bounded context, one team (Conway's Law, deliberately)#

Vernon explicitly invokes Conway's Law: the structure of the software will mirror the structure of the organization that builds it, whether you plan for that or not. The practical move is to plan for it deliberately — assign one team per bounded context (or one bounded context per team, for larger contexts that need more than a handful of people), so that team ownership and model ownership are the same boundary. A shared bounded context owned by two teams tends to accumulate compromises neither team is happy with, because neither has full authority over the model.

Worked example — an online collaboration/scrum tool (Vernon's running example). The system splits into contexts including Collaboration (forums, discussions, calendars), Identity and Access (users, roles, tenants), and Agile Project Management (product backlogs, sprints, backlog items). Each is owned by a distinct team, has its own model of "User" or "Product" tailored to its own concerns (Identity and Access's User cares about credentials and roles; Agile PM's equivalent concept, if it needs one at all, cares about story assignment), and the two are integrated deliberately (implementing-ddd/12) rather than sharing a table.

Rule 2 — One bounded context, one persistence boundary#

No shared database schema across bounded contexts. Each context owns its own schema (or database instance), because a shared schema is a shared model whether or not the code admits it — any other context's code that queries your tables directly is coupled to your internal representation, and you can no longer change that representation without a cross-team migration. This is the same principle that later became "database per service" in microservices architecture, arrived at independently from the modeling side rather than the deployment side.

Rule 3 — Draw the boundary where the language changes#

The operational signal for where a bounded context boundary belongs: watch for a word whose meaning silently shifts depending on who's using it. In an e-commerce platform, "Product" in the Catalog context means a sellable item with descriptions, images, and category — in the Inventory context, the same word denotes a stock-keeping unit with quantity-on-hand and warehouse location — in the Pricing context, it denotes something with a price history and applicable discount rules. Trying to force one shared Product class to serve all three contexts produces a bloated, incoherent class serving three masters badly; splitting them (even duplicating some fields, like a product's name, across contexts) keeps each context's model sharp and lets each evolve independently.

Rule 4 — Bounded contexts communicate through explicit contracts, not shared code#

Since contexts don't share a database or a model, they need an explicit way to exchange information: published language, translation layers, and messaging (implementing-ddd/10, implementing-ddd/11, implementing-ddd/12). This is the cost side of the autonomy trade-off — integration requires deliberate design work that a shared-everything monolith doesn't need, but that design work is what buys each context the freedom to evolve its model without coordinating every change with every other team.

Recognizing context boundaries in an existing (non-DDD) system#

Vernon also covers the reverse problem: given a legacy system with no explicit boundaries, how do you find where they should be? Look for: naming collisions and near-misses (the same word used loosely for different things), modules that different teams already treat as "theirs" informally, and places where a single database table is queried by unrelated parts of the system for unrelated purposes — each is a signal of where a boundary wants to exist, even if the code doesn't reflect it yet.

Pros#

  • Autonomy at the bounded-context level lets teams move independently — deploy on their own schedule, choose their own storage technology, evolve their model without a cross-team migration — which is the single biggest practical benefit DDD strategic design offers to an organization, not just to individual developers.
  • Keeping each context's ubiquitous language pure (not diluted to satisfy every other context's needs) keeps the model genuinely useful for reasoning about that context's actual domain, instead of degrading into a lowest-common-denominator shared vocabulary.
  • Makes the core/supporting/generic distillation from implementing-ddd/01 actionable: once contexts are separately deployable, you can genuinely invest differently in each (rewrite the core context in a richer style, buy a generic context outright) without disturbing the rest of the system.

Cons#

  • Enforcing "no shared schema" has real short-term cost: duplicated reference data (e.g. a product name cached in three contexts), eventual consistency between contexts instead of a single transactional read (implementing-ddd/06), and genuine integration engineering effort (implementing-ddd/12) that a shared database would have avoided for free.
  • Getting context boundaries wrong is expensive to fix later — too many small contexts creates integration overhead disproportionate to the value each context provides; too few (a "big ball of mud" context) reintroduces exactly the model pollution bounded contexts exist to prevent.
  • Requires organizational buy-in beyond engineering — Conway's Law alignment means team structure itself may need to change, which is a political and staffing decision, not a purely technical one, and often outside a single engineer's or even a single engineering leader's control.

Alternatives#

  • Modular monolith with enforced module boundaries — keep everything in one deployable but enforce bounded-context discipline (no cross-module direct persistence access, explicit module-to-module interfaces) at the code level via build tooling; captures much of the modeling discipline without the operational cost of distributed deployment, at the cost of teams still sharing a release train.
  • Shared kernel — an explicit, small, deliberately-shared subset of the model between two closely collaborating contexts (see context mapping patterns), used when the integration cost of full separation outweighs the benefit for a specific pair of tightly coupled contexts; requires the two teams to coordinate any change to the shared part, so it's used sparingly.
  • Single shared model, no bounded contexts — the default in many non-DDD systems: one Order, one Product, used everywhere; simpler at small scale, but degrades as the system and team grow, since it accumulates exactly the pollution and the whose's-responsibility-is-this ambiguity bounded contexts are designed to prevent.

When to use it#

As soon as a system has more than one team, more than one genuinely distinct sub-language, or more than one differently-evolving part of the domain (per the distillation from implementing-ddd/01) — draw explicit bounded context boundaries and give each team ownership of its own model and schema.

When NOT to use it#

For a small system built and owned by a single small team with one coherent domain and no near-term plan to split ownership, formal bounded-context separation (separate schemas, separate deployables, translation layers) is premature complexity — a modular monolith with informal discipline is enough until the team or domain actually grows past what one shared model can bear.

Key takeaways / mental model#

A bounded context is a promise: "inside this boundary, this word means exactly one thing, and no other team can silently change that meaning out from under you." That promise only holds if the boundary is also a boundary of team ownership and deployment — a linguistic boundary alone, without structural enforcement, degrades back into a shared muddled model the moment two teams touch the same schema under deadline pressure.

Self-check questions#

  1. Find a word in a system you've worked on whose meaning subtly differs depending on which part of the codebase uses it (e.g. "Customer," "Order," "Account"). Would that be a signal for a bounded context boundary? Why or why not?
  2. Why does Vernon insist bounded context boundaries align with team boundaries, rather than being a purely technical/architectural decision made by an architect?
  3. Describe a realistic cost of enforcing "no shared database schema" between two bounded contexts that need to display related data together (e.g. a checkout page showing both catalog and inventory data). How would you address it without breaking the boundary?
  4. Given a legacy monolith with a single shared User table used by five different features for five different purposes, how would you go about identifying where the bounded context boundaries actually want to be?

References#

  • Implementing Domain-Driven Design (Vaughn Vernon), Chapter 2: "Domains, Subdomains, and Bounded Contexts".
  • Domain-Driven Design (Eric Evans) — the original Bounded Context definition; see ddd-evans.