Loading learning content...
Understanding bounded contexts conceptually is one thing; actually defining their boundaries in a real system is another challenge entirely. Where exactly does the Sales context end and the Billing context begin? Should Order Management be its own context, or part of Sales? These decisions have profound implications for system architecture, team organization, and long-term maintainability.
Context boundary definition is both a technical and organizational exercise. The boundaries we draw in our software often reflect—or should reflect—boundaries in our organization. Getting this alignment right is crucial for sustainable software development at scale.
This page equips you with practical techniques for discovering, defining, documenting, and enforcing context boundaries in real-world systems.
By the end of this page, you will understand how to analyze domains to discover natural context boundaries, how to document and communicate these boundaries clearly, and how to enforce them in code and infrastructure. You'll learn both discovery techniques and boundary enforcement patterns.
Boundaries don't emerge from code examination—they emerge from domain understanding. The most effective way to discover context boundaries is through deep engagement with domain experts and careful analysis of the business itself.
Event Storming for Boundary Discovery
Event Storming is a collaborative workshop technique particularly effective for discovering bounded contexts. In an Event Storming session:
These swimlanes often correspond to bounded context boundaries. When you notice that a group of events is discussed by one set of experts and another group by different experts, you've found a likely boundary.
1234567891011121314151617181920212223242526272829
Event Storm Timeline (abbreviated)═══════════════════════════════════════════════════════════════════════════════════════════ ┌─────────────────────────────────────────────────────────────────────────────────────────┐│ ││ SALES EXPERTISE FULFILLMENT EXPERTISE ││ (Discussed by sales team) (Discussed by operations team) ││ ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ Lead │ │ Lead │ │ Quote │ │ Order │ │ Items │ │ Shipment │ ││ │ Captured │→ │ Qualified│→ │ Accepted │ ★★ │ Received │→ │ Picked │→ │ Dispatched│ ││ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││ │ │ ││ │ PIVOTAL │ ││ │ EVENT │ ││ └──────────────┘ ││ ││ ★★ = Pivotal Event: "Order Placed" marks transition from Sales to Fulfillment ││ ││ NATURAL BOUNDARY: Events before "Order Placed" belong to Sales Context ││ Events after "Order Placed" belong to Fulfillment Context ││ │└─────────────────────────────────────────────────────────────────────────────────────────┘ Key Observations:• Different experts discuss different clusters of events• Pivotal events mark transitions between contexts• The same "Order" entity has different meanings before and after the pivotal event• Commands and aggregates cluster naturally around these event groupsPivotal Events as Boundary Markers
Pivotal events are particularly valuable for boundary discovery. These are domain events that represent significant state transitions in the business domain. Common pivotal events include:
Each pivotal event often marks a natural context boundary. The contexts before and after the event typically have different owners, different concerns, and different models.
Conway's Law states that organizations design systems that mirror their communication structures. This observation has profound implications for bounded context design:
"Any organization that designs a system will inevitably produce a design whose structure is a copy of the organization's communication structure." — Melvin Conway, 1967
Rather than fighting Conway's Law, DDD practitioners embrace it strategically. Bounded contexts should align with team structures. When the software boundary matches the organizational boundary, communication overhead is minimized and ownership is clear.
Some organizations deliberately restructure their teams to produce a desired architecture. If you want four bounded contexts, organize into four teams. The team structure will naturally produce the architectural boundaries you seek. This is called the 'Inverse Conway Maneuver' or 'Conway's Law in reverse'.
| Pattern | Description | When to Use | Risks |
|---|---|---|---|
| One Team, One Context | A single team owns exactly one bounded context | Most common and recommended. Clear ownership, clear boundaries. | Team becomes bottleneck if context grows too large |
| One Team, Multiple Contexts | A single team owns several small bounded contexts | Small related contexts that don't justify separate teams | Contexts may blur together without discipline |
| Multiple Teams, One Context | Several teams collaborate on a single large bounded context | Large, complex contexts that require significant engineering capacity | Requires exceptional coordination; model consistency at risk |
| Platform Team Model | Dedicated team provides context as a service to others | Core capabilities (auth, payments) used by many contexts | Platform team can become bottleneck; must have clear API contracts |
Signs of Misalignment
When context boundaries don't align with team structures, several problems emerge:
If you observe these symptoms, consider whether your context boundaries match your organizational boundaries. Often, realigning either the contexts or the teams resolves these issues.
Context boundaries can be enforced at different levels of the technology stack. Understanding these levels helps you choose the right enforcement mechanisms for your situation.
The Boundary Enforcement Spectrum
Boundaries exist on a spectrum from purely logical (convention-based) to fully physical (network-isolated). The right choice depends on your organization's maturity, system complexity, and operational capabilities.
1234567891011121314151617181920
WEAKER ENFORCEMENT ←←←←←←←←←←←←←←←←←←←←←←←→→→→→→→→→→→→→→→→→→→→→→ STRONGER ENFORCEMENT ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ Conventions │ → │ Packages/ │ → │ Module │ → │ Separate │ → │ Separate ││ & Reviews │ │ Namespaces │ │ Systems │ │ Repos │ │ Services │└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ Human trust Compiler may Compiler/build Build system Runtime required warn but allows enforces enforces enforces violations boundaries boundaries boundaries Characteristics:• LEFT SIDE: Easy to start, hard to maintain, discipline degrades over time• RIGHT SIDE: Hard to set up, easy to maintain, boundaries naturally preserved Recommendation:• Start with module systems in monolith• Move to separate services when contexts prove stable• Never start with microservices before contexts are clearMany teams jump directly to physical boundaries (microservices) before understanding their domain. This locks in arbitrary boundaries that are expensive to change. Start with a modular monolith with logical boundaries. Once boundaries prove stable (typically 6-12 months), consider extracting to physical boundaries if beneficial.
Logical boundaries require code-level enforcement to be effective. Without enforcement, boundaries degrade over time as developers take shortcuts. Here are techniques for maintaining boundary integrity in code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Root tsconfig.json - orchestrates all bounded contexts{ "files": [], "references": [ { "path": "./contexts/sales" }, { "path": "./contexts/billing" }, { "path": "./contexts/fulfillment" }, { "path": "./contexts/shared-kernel" } // Shared types across contexts ]} // contexts/sales/tsconfig.json{ "compilerOptions": { "composite": true, "rootDir": "./src", "outDir": "./dist", "declaration": true, "declarationMap": true }, "include": ["src/**/*"], "references": [ // Sales can only reference shared-kernel // It CANNOT reference billing or fulfillment { "path": "../shared-kernel" } ]} // contexts/billing/tsconfig.json{ "compilerOptions": { "composite": true, "rootDir": "./src", "outDir": "./dist" }, "include": ["src/**/*"], "references": [ // Billing also only references shared-kernel { "path": "../shared-kernel" } ]} // KEY: If sales tries to import from billing, TypeScript will error:// import { Invoice } from '../billing/src/invoice'; // ❌ ERROR!// The project reference system enforces that only declared references are allowed1234567891011121314151617181920212223242526272829303132333435
// contexts/sales/src/module-info.javamodule com.company.sales { // Explicitly export public API exports com.company.sales.api; // Internal packages are NOT exported // Other modules cannot access internal classes // Declare dependencies requires com.company.sharedkernel; // Note: NO dependency on billing or fulfillment // The module system enforces this at compile time} // contexts/billing/src/module-info.javamodule com.company.billing { exports com.company.billing.api; requires com.company.sharedkernel; // If we tried to add: requires com.company.sales; // We could, but it would be a conscious decision visible in module-info // Code review catches unintended dependencies} // contexts/fulfillment/src/module-info.javamodule com.company.fulfillment { exports com.company.fulfillment.api; requires com.company.sharedkernel; // Integration with other contexts happens through: // 1. Events (async, loosely coupled) // 2. Anti-corruption layer adapters // 3. Shared kernel types only}Architecture Fitness Functions
Architecture fitness functions are automated tests that verify architectural constraints, including bounded context boundaries. They run in CI/CD pipelines and fail builds that violate boundaries.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
import com.tngtech.archunit.core.domain.JavaClasses;import com.tngtech.archunit.core.importer.ClassFileImporter;import com.tngtech.archunit.lang.ArchRule;import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; public class BoundedContextBoundaryTests { private JavaClasses classes = new ClassFileImporter() .importPackages("com.company"); @Test void sales_context_should_not_depend_on_billing_internals() { ArchRule rule = noClasses() .that().resideInAPackage("..sales..") .should().dependOnClassesThat() .resideInAPackage("..billing.internal.."); rule.check(classes); } @Test void bounded_contexts_should_only_access_other_contexts_through_published_api() { ArchRule rule = noClasses() .that().resideInAPackage("..sales..") .should().dependOnClassesThat() .resideInAnyPackage( "..billing.domain..", // Internal domain model "..billing.application..", // Internal application services "..billing.infrastructure.." // Internal infrastructure ); // Only billing.api is allowed rule.check(classes); } @Test void aggregate_roots_should_not_be_exposed_across_context_boundaries() { ArchRule rule = classes() .that().areAnnotatedWith(AggregateRoot.class) .should().notBePublic() .orShould().resideInAPackage("..internal.."); rule.check(classes); } @Test void context_integration_should_use_integration_events_only() { // Contexts should communicate via events, not direct method calls ArchRule rule = classes() .that().resideInAPackage("..sales..") .and().implement(EventHandler.class) .should().onlyHaveInputParameters() .that().areAssignableTo(IntegrationEvent.class); rule.check(classes); }}One of the most debated aspects of bounded context boundaries is data ownership. Should each context have its own database? Can contexts share a database with schema separation? These decisions have significant implications for data integrity, query complexity, and operational cost.
| Pattern | Description | Pros | Cons |
|---|---|---|---|
| Shared Database | All contexts use same database, any table accessible | Simple queries, no sync needed, familiar | Tight coupling, model leakage, ownership unclear |
| Schema per Context | Each context has own schema within shared database | Some isolation, cross-schema queries possible | Still coupled at DB level, schema boundaries often violated |
| Database per Context | Each context has completely separate database | Full isolation, independent scaling, technology freedom | Cross-context queries impossible, sync complexity |
| Polyglot Persistence | Each context chooses optimal database technology | Best fit per context, optimized performance | Operational complexity, multiple technologies to manage |
12345678910111213141516171819202122232425262728293031323334353637383940
PHASE 1: Modular Monolith (Schema Separation)═════════════════════════════════════════════════════════════════════════════ ┌─────────────────────────────────────┐ Application │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Sales │ │ Billing │ │Fulfillm.│ │ │ │ Module │ │ Module │ │ Module │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ └───────┼───────────┼───────────┼──────┘ │ │ │ ▼ ▼ ▼ ┌───────────────────────────────────────┐ Database │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ (PostgreSQL) │ │ sales. │ │billing. │ │fulfill. │ │ │ │ * │ │ * │ │ * │ │ │ └─────────┘ └─────────┘ └─────────┘ │ └───────────────────────────────────────┘ • Each module accesses ONLY its own schema• Foreign keys across schemas DISCOURAGED• Cross-context data via events, not joins ═════════════════════════════════════════════════════════════════════════════ PHASE 2: Service Extraction (Database per Service)═════════════════════════════════════════════════════════════════════════════ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Sales │ │ Billing │ │Fulfillm.│ │ Service │ │ Service │ │ Service │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │PostgreSQL│ │PostgreSQL│ │ MongoDB │ ← Polyglot! │ (sales) │ │(billing)│ │(fulfill)│ └─────────┘ └─────────┘ └─────────┘ • Each service owns its database exclusively• Other services CANNOT access directly• Communication via API calls or events• Schema/technology can evolve independentlyDatabase-per-context means accepting strategic data duplication. The Billing context keeps its own copy of customer name and address, synced via events from the master in Sales. This duplication enables context autonomy and is a feature, not a bug. The key is defining which context is the 'source of truth' for each piece of data.
Context boundaries must be documented and communicated, not just encoded in technical mechanisms. Good documentation ensures that current and future team members understand why boundaries exist and how to respect them.
Context Map
A Context Map is the primary documentation artifact for bounded contexts. It shows:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
╔══════════════════════════════════════════════════════════════════════════════════════╗║ E-COMMERCE PLATFORM CONTEXT MAP ║╠══════════════════════════════════════════════════════════════════════════════════════╣║ ║║ ┌───────────────────────┐ ┌───────────────────────┐ ║║ │ SALES CONTEXT │ │ CATALOG CONTEXT │ ║║ │ ─────────────── │ │ ──────────────── │ ║║ │ Team: Sales Eng │ │ Team: Merch Tech │ ║║ │ Owner: Alice │←──ACL────→│ Owner: Bob │ ║║ │ │ │ │ ║║ │ Core Domain │ │ Supporting │ ║║ └───────────┬───────────┘ └───────────────────────┘ ║║ │ ║║ │ Upstream/Downstream ║║ │ (Sales publishes OrderPlaced ║║ │ events downstream) ║║ ▼ ║║ ┌───────────────────────┐ ┌───────────────────────┐ ║║ │ FULFILLMENT CONTEXT │←─Events──→│ BILLING CONTEXT │ ║║ │ ────────────────── │ │ ──────────────── │ ║║ │ Team: Ops Eng │ │ Team: Finance Tech │ ║║ │ Owner: Carol │ │ Owner: David │ ║║ │ │ │ │ ║║ │ Core Domain │ │ Core Domain │ ║║ └───────────┬───────────┘ └───────────────────────┘ ║║ │ ║║ │ Customer/Supplier ║║ │ (Fulfillment consumes ║║ │ Shipping Partner API) ║║ ▼ ║║ ┌───────────────────────┐ ║║ │ SHIPPING PARTNER │ ║║ │ (External System) │ ║║ │ ────────────────── │ ║║ │ Conformist: We use │ ║║ │ their model as-is │ ║║ └───────────────────────┘ ║║ ║║ Legend: ║║ ─ACL─ = Anti-Corruption Layer (translation) ║║ ─Events─ = Event-based integration ║║ U/D = Upstream/Downstream (power relationship) ║║ C/S = Customer/Supplier (negotiated contract) ║║ ║╚══════════════════════════════════════════════════════════════════════════════════════╝Context Documentation Template
Each bounded context should have documentation covering:
Even with good enforcement mechanisms, boundary violations sometimes occur or are attempted. Here's how to handle them constructively:
When Developers Attempt Violations
Developers sometimes try to bypass boundaries because they seem to add friction. Common complaints:
These complaints are often valid experiences of friction. The solution isn't to dismiss them but to address the root cause:
A single shortcut across context boundaries sets precedent. The next developer thinks, 'They did it, so it must be okay.' Within months, boundaries are riddled with exceptions. Treat every violation seriously—either it reveals a boundary problem (fix the boundary) or it's a discipline problem (enforce the boundary).
When Boundaries Need Adjustment
Sometimes friction indicates that boundaries are wrong. Signs that boundaries need adjustment:
Boundary adjustment is a normal part of domain learning. Don't treat initial boundaries as sacred. As understanding deepens, boundaries may shift.
We've covered the practical aspects of context boundary definition and maintenance. Here are the essential takeaways:
What's Next:
With boundaries defined, we need to understand how bounded contexts relate to each other. The next page explores Context Mapping—the patterns and relationships that describe how bounded contexts integrate, communicate, and influence each other.
You now understand how to discover, define, and enforce context boundaries in real-world systems. Next, we'll explore the rich vocabulary of context relationships—the patterns that describe how bounded contexts work together.