Menu
Module 4 / 8 Intermediate 15 min read

Bounded Contexts

The explicit boundary within which one model and one language hold unambiguously — and why the same word can mean two different things on either side of it.

In this module

Define the Bounded Context, the central strategic pattern in DDD, and learn the heuristics for locating real boundaries in a codebase.

The central strategic pattern

A Bounded Context is an explicit boundary inside which a particular model — and the Ubiquitous Language that goes with it — applies consistently and without ambiguity. It is, without much competition, the single most important pattern in strategic DDD: get your context boundaries right and a confusing system becomes legible; get them wrong (or skip drawing them at all) and no amount of careful class design inside will save you.

The defining trick of a Bounded Context is this: the same word can mean genuinely different things in different contexts, and that’s not a bug — it’s the model working correctly. In the bookstore, Customer in the Sales context is “whoever placed this order” — identified, with a default shipping address, nothing more. If the bookstore later added a marketing context, Customer there might be a prospect with an email-engagement score and a list of abandoned carts — a completely different shape, different fields, different rules, same English word. Trying to force both into one shared Customer class is precisely the mistake Bounded Contexts exist to prevent: it produces a class bloated with fields half the codebase doesn’t need, owned by no one in particular, that breaks in one context every time it’s changed for another.

A Bounded Context is a solution-space boundary — how you've chosen to organize the software. A subdomain is a problem-space boundary — how the business itself is shaped. In a tidy design they line up one-to-one. In real systems, they often don't, and noticing where they diverge is itself useful information.

Where the bookstore’s contexts land

This course treats the bookstore as three Bounded Contexts, matching the three areas introduced in Module 1:

  • Sales — placing orders, the Order aggregate, the core domain.
  • Billing — generating and tracking invoices once an order exists.
  • Shipping — turning an order into one or more shipments and getting them to the customer.

Each owns its own version of any shared-sounding word. Order is referenced in Billing and Shipping, but neither owns it or holds the full Order object — they hold an OrderId and react to facts Sales publishes (you’ll see exactly how in Module 5 and again with Domain Events in Module 8). That’s the discipline: a context can know about another context’s concepts, but it shouldn’t absorb them.

Heuristics for finding the boundary

Real codebases rarely have a label that says “context boundary here.” You infer it, in roughly this priority order:

  1. Deployment and module boundaries. Separate services, separate Maven/Gradle modules, or distinct top-level Java packages (com.bookstore.sales, com.bookstore.billing) are the strongest signal you’ll get. If someone already drew a line, take it seriously before second-guessing it.
  2. The same name, different shapes. If Customer exists in two packages with different fields and different methods, you’ve found two contexts meeting — that’s the Customer example above, made concrete.
  3. Distinct persistence. Separate schemas, separate datasources, or sets of @Entity classes that never reference each other’s tables, suggest the data itself has already been partitioned along context lines.
  4. Translation code at the edges. A mapper, adapter, or façade that converts one package’s types into another’s is a strong tell — nobody writes a translator between two parts of the same model. (This is also your first hint of an Anticorruption Layer, the subject of Module 5.)
  5. Cohesion of language. A cluster of types that constantly reference each other and share vocabulary, with only sparse references pointing outward, behaves like one context even without a clean module boundary to prove it.

For each context you find, it’s worth writing down its name, the module(s) it lives in, its core concepts, and the dominant vocabulary it speaks — this is the glossary from Module 2, scoped to one boundary.

Note. Signal 1 (module boundaries) and signal 2 (repeated names) sometimes disagree — you’ll find Customer duplicated inside what looks like a single package. Trust the language cohesion over the folder structure when that happens; packages get reorganized for all kinds of non-domain reasons, but a word genuinely meaning two different things doesn’t lie.

A common mistake: one context per database table

It’s tempting to let your ORM decide your contexts for you — one @Entity, one table, one “context.” Resist this. A Bounded Context is about where a model and its language hold consistently, not about how data happens to be normalized. OrderLine lives inside the Order aggregate in the Sales context even though it might have its own table; it doesn’t become a context of its own just because it’s persisted separately (you’ll see exactly why when Aggregates are introduced in Module 7). Boundaries are about meaning, not about schema.

With the contexts identified, the next question is unavoidable: how do Sales, Billing, and Shipping actually talk to each other, and who’s in charge of the shared facts between them? That’s Context Mapping.

Next: Module 5 — Context Mapping.