Back to Insights
domain-driven designJuly 25, 20269 min read

Domain-Driven Design: Finding Practical Business Boundaries

A field guide to discovering bounded contexts through language, invariants, workflows, data ownership, and organizational evidence—without turning DDD into modeling ceremony.

John Mather, Founder of Prime Axiom AI
John Mather

Founder, Prime Axiom AI

Published July 25, 2026

Business bounded contexts connected through explicit event contracts
Practical bounded contexts align language, invariants, data, and team ownership.

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.

ConversationLocal termIdentityLifecycle trigger
Salesprospect/customerCRM organizationquote accepted
Billingaccountlegal billing entitybilling profile approved
Productworkspacetenant identifierprovisioning completed
Supportrequesterverified contactticket 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:

mermaid
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:

  1. Quoting — offers, price proposals, discount approval, acceptance.
  2. Billing — billing accounts, invoices, payments, credits, tax evidence.
  3. Provisioning — workspace resources and activation status.
  4. Entitlements — effective feature grants and exceptions.
  5. 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:

ts
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:

json
{
  "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

mermaid
flowchart LR
  Quoting -- "QuoteAccepted" --> Billing
  Billing -- "BillingAccountApproved" --> Provisioning
  Provisioning -- "WorkspaceProvisioned" --> Entitlements
  Entitlements -- "SubscriptionActivated" --> Support
  Identity[Identity Provider] --> ACL[Identity Anti-Corruption Layer]
  ACL --> Support

The 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 choiceCouplingUse whenAvoid when
Shared kernelhigh organizationaltiny stable semantics, close teamsrelease autonomy matters
Published API/languageexplicit contractclear upstream ownershipmeaning changes per consumer
Anti-corruption layertranslation costlegacy/vendor model differsmodels are already identical
Conformistdownstream dependenceupstream cannot adaptcore 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.

Sources

Apply the idea

Turn technical clarity into a working system.

Talk with Prime Axiom about architecture, AI automation, integrations, and the operational workflow behind your next build.

Continue reading

Visible dependency graph assembled from constructors and adapters
A composition root makes implementation choices and resource lifetimes explicit.
dependency injection

July 25, 2026 · 8 min read

Dependency Injection Without Framework Magic

Plain constructors, factories, and one visible composition root provide dependency injection’s core benefits without hidden container behavior or framework coupling.

By John Mather
Read article →