Domain-Driven Design: Finding Practical Business Boundaries
Domain-Driven Design is most valuable where software carries complicated, changing business knowledge. Its strategic contribution is a way to decide where one model stops and another begins. The same word can mean different things in different parts of a company, and forcing one universal model often creates more coupling than consistency.
This is a boundary-discovery field guide. It follows a subscription company whose teams disagree about “customer,” “plan,” and “activation.” The goal is not to produce perfect diagrams. It is to discover cohesive ownership that can be implemented first as modules and split into services only when operational forces justify it.
Eric Evans introduced bounded contexts and ubiquitous language in Domain-Driven Design. Martin Fowler's overview of bounded contexts summarizes the need to define the applicability of each model. Microsoft's DDD guidance connects domain analysis to service boundaries while warning that boundaries should follow business capabilities rather than technical layers.
Field note 1: collect language collisions
Interview people around real work, not abstract nouns. Ask support to walk through a cancellation, finance to reconcile an invoice, sales to create a quote, and product to activate an account. Write verbs, decisions, exceptional cases, and artifacts.
In the subscription company:
- Sales calls an organization with an accepted quote a customer.
- Billing calls the legal entity responsible for invoices an account.
- Product calls a provisioned tenant with active users a workspace.
- Support uses customer for the person opening a ticket.
None is universally wrong. A global Customer entity would accumulate optional billing addresses, workspace settings, quote state, ticket identity, and contradictory lifecycle rules. The collision is evidence of context boundaries.
| Conversation | Local term | Identity | Lifecycle trigger |
|---|---|---|---|
| Sales | prospect/customer | CRM organization | quote accepted |
| Billing | account | legal billing entity | billing profile approved |
| Product | workspace | tenant identifier | provisioning completed |
| Support | requester | verified contact | ticket opened |
The table demonstrates that identity and lifecycle differ along with vocabulary. Translation between contexts is healthier than a shared mega-object.
Field note 2: map decisions and invariants
Boundaries form around rules that must remain consistent. Billing owns: an invoice total equals valid line items, taxes, credits, and currency rules. Product provisioning owns: a workspace cannot be Active until required resources exist. Sales quoting owns discount authority and quote expiration.
Write invariants as sentences:
Quoting: an accepted quote cannot be repriced. Billing: an issued invoice cannot be edited; corrections use credit notes. Provisioning: activation requires every mandatory resource. Entitlements: effective features follow the purchased offer and active exceptions.
This ASCII list demonstrates separate transactional truths. If one operation must keep an invariant atomically, the responsible state usually belongs in one aggregate and one consistency boundary. Do not make every related workflow one aggregate; long business processes can coordinate multiple contexts asynchronously.
Event-storm a difficult workflow
Use past-tense business events, commands, policies, and questions. A simplified activation flow:
flowchart LR
A[Quote Accepted] --> B[Create Billing Account]
B --> C[Billing Account Approved]
C --> D[Provision Workspace]
D --> E[Workspace Provisioned]
E --> F[Grant Entitlements]
F --> G[Subscription Activated]The diagram demonstrates a business sequence, not a synchronous call chain. Each event can mark a handoff between models. Ask what happens when billing approval is delayed, provisioning partially fails, or entitlement grant must be reversed. Those questions reveal process ownership and compensation.
Avoid treating every sticky note as a production event. First discover language and responsibility. Later classify which transitions need durable integration messages, which are local domain events, and which are merely implementation details.
Sketch candidate bounded contexts
Candidate contexts might be:
- Quoting — offers, price proposals, discount approval, acceptance.
- Billing — billing accounts, invoices, payments, credits, tax evidence.
- Provisioning — workspace resources and activation status.
- Entitlements — effective feature grants and exceptions.
- Support — requests, contacts, service-level policies.
Each context needs an explicit purpose, language, owned data, incoming commitments, and outgoing facts. If a proposed context is called “Database” or “API,” it is a technical layer, not a business boundary.
Boundary test: can a business expert describe the decisions this context owns without naming another context's tables?
Model one context deeply
Inside Quoting, an Offer may be a value object and Quote an aggregate:
class Quote {
private status: QuoteStatus = 'draft';
private lines: QuoteLine[] = [];
accept(by: Buyer, at: Instant): QuoteAccepted {
if (this.status !== 'sent') throw new QuoteNotAcceptable(this.status);
if (at.isAfter(this.expiresAt)) throw new QuoteExpired();
if (!buyerMatches(by, this.recipient)) throw new WrongBuyer();
this.status = 'accepted';
return new QuoteAccepted(this.id, this.billingProposal(), at);
}
}This block demonstrates a local model with local language. It does not set a BillingAccount row or create a Workspace. It emits a fact containing the minimum stable data needed for the handoff. Quoting remains authoritative for quote acceptance; Billing interprets that event using its own model.
The integration event should not serialize the entire Quote aggregate:
{
"eventId": "evt_...",
"type": "QuoteAccepted.v1",
"quoteId": "quote_...",
"buyerOrganizationId": "org_...",
"currency": "USD",
"billingProposal": {
"cadence": "monthly",
"netAmountMinor": 250000
},
"occurredAt": "2026-07-25T12:00:00Z"
}This code block demonstrates an integration contract focused on a business fact. Versioning is explicit, money includes currency and minor-unit representation, and identity is stable. Billing may reject or request missing tax information through its own workflow.
Draw the context map
flowchart LR
Quoting -- "QuoteAccepted" --> Billing
Billing -- "BillingAccountApproved" --> Provisioning
Provisioning -- "WorkspaceProvisioned" --> Entitlements
Entitlements -- "SubscriptionActivated" --> Support
Identity[Identity Provider] --> ACL[Identity Anti-Corruption Layer]
ACL --> SupportThe context map demonstrates relationship types. Upstream contexts publish facts; downstream contexts translate. An anti-corruption layer around an external identity model prevents provider roles and claims from becoming Support's domain language.
Not every integration must be asynchronous. A query for a current tax rate may be synchronous if the caller can tolerate availability coupling. A durable workflow event may be asynchronous because the handoff must survive downtime. Choose interaction styles from consistency and failure requirements.
Decide what to share
Shared kernels—code or schemas jointly owned by contexts—reduce duplication but introduce coordination. Share only concepts that truly have identical meaning and lifecycle. A generic CurrencyCode or Instant type may be safe. A shared Customer class rarely is.
Prefer published language for integration: documented messages or APIs owned by the upstream context. Downstream contexts maintain translation. Duplication of small value types can be cheaper than synchronized releases.
| Sharing choice | Coupling | Use when | Avoid when |
|---|---|---|---|
| Shared kernel | high organizational | tiny stable semantics, close teams | release autonomy matters |
| Published API/language | explicit contract | clear upstream ownership | meaning changes per consumer |
| Anti-corruption layer | translation cost | legacy/vendor model differs | models are already identical |
| Conformist | downstream dependence | upstream cannot adapt | core competitive model is polluted |
The table demonstrates that context relationships include organizational power, not just code.
Validate boundaries with change scenarios
Run proposed changes through the map:
- Introduce annual prepayment with monthly entitlement periods.
- Add manual credit review for large contracts.
- Support workspace activation before the first invoice is collected.
- Replace the CRM while preserving quoting history.
- Apply a temporary feature exception without altering the commercial offer.
Count contexts touched and ask whether each touch reflects a real business consequence. If a CRM replacement changes Billing tables, a boundary leaks. If annual prepayment requires coordinated changes to Quoting, Billing, and Entitlements, that may be legitimate—but contracts should isolate each interpretation.
Also compare team ownership. A boundary split across three teams will struggle to preserve a coherent model. A team owning unrelated contexts may become overloaded. Conway's law is not destiny, but communication patterns influence system structure.
Implement boundaries before distributing them
Bounded context does not mean microservice. Start with modules in one deployable application:
src/ quoting/ domain/ application/ adapters/ billing/ domain/ application/ adapters/ provisioning/ entitlements/
The directory view demonstrates business-oriented modules. Enforce import rules: Billing cannot access Quoting repositories or tables; it consumes Quoting's public contract. Each module may own a schema or table prefix and expose application commands and queries.
This modular monolith provides transactions and simple deployment while preserving extraction options. The Modular Monolith gives a fuller implementation path. Split only when independent scaling, deployment cadence, security isolation, or team autonomy outweigh network and operations costs.
Testing the model and its translations
Domain tests should use examples supplied by experts: expired quote, unauthorized discount, credit-note correction, partial provisioning. Property-based tests can check money invariants and state-machine transitions. Application tests cover command coordination.
Contract tests verify integration event schemas and semantics. Consumer tests should not freeze every optional field; they should assert what the consumer relies on. Replay representative historical events through new consumers before deployment. Anti-corruption layers need mapping tests for unknown provider values and missing fields.
In production, monitor business flow: time from QuoteAccepted to SubscriptionActivated, counts waiting in each state, retries, and manual interventions. Technical queue depth is useful but does not tell operators which subscriptions are stuck. Correlation should use stable business identifiers without leaking sensitive fields.
Boundary failure patterns
Entity-first modeling: teams debate every property of Customer before mapping decisions. Start with workflows and invariants.
One vocabulary by decree: a canonical enterprise model forces local meanings into optional fields and status flags.
Database ownership without behavior ownership: schemas are separated, but business rules still live in a shared service.
Event soup: contexts broadcast generic EntityUpdated events, making consumers understand private state.
Premature services: tentative boundaries become network APIs before the language stabilizes, making refactoring expensive.
DDD everywhere: a stable CRUD reference table does not need aggregates, factories, and domain services. Invest where complexity and change justify modeling.
Workshop exit checklist
- [ ] Important language collisions are recorded with real examples.
- [ ] Each context has a purpose, model, and owned invariants.
- [ ] Cross-context workflows expose failure and delay.
- [ ] Integration contracts carry business facts, not internal objects.
- [ ] Context relationships and team ownership are explicit.
- [ ] Candidate boundaries survive several change scenarios.
- [ ] Modules enforce boundaries before services are considered.
- [ ] Business-flow telemetry can reveal stuck work.
Practical boundaries are hypotheses
The subscription map is not final truth. As experts learn, a Billing/Collections split may emerge, or Entitlements may prove part of Product Provisioning. A good boundary makes current language and responsibility explicit enough to test. It can evolve through parallel contracts and data migration.
DDD's value is not elaborate diagrams. It is the discipline of placing models where their language is coherent, protecting invariants within clear ownership, and translating at unavoidable seams. Treat boundaries as evidence-based hypotheses, implement them with modules first, and use operational feedback to refine them. For complementary guidance, see Clean Architecture Boundaries, Event-Driven Architecture, and /resources.


