Field guide · strategic → tactical

Domain-Driven Design, the parts you actually use

DDD is a budget for thinking. First decide where careful modeling pays (strategic design), then model carefully only there (tactical design). This page is the whole path, in the order you walk it.

01

Two spaces, one rule

The problem space is the business as it is: its activities, split into subdomains. The solution space is your response: models drawn inside bounded contexts. They often map one-to-one — but they don't have to, so never force it.

The one rule The domain leads, the model follows, the code follows the model. Never start from the database schema, the framework, or the folder layout — start from how the business works and the words its experts use.
02

Triage the subdomains

Classify each part of the business before you invest a single design hour. The classification is the budget decision.

Core

Your competitive edge — complex and valuable. The thing you'd never outsource; the reason customers pick you.

Best people, richest modeling. DDD earns its keep here.

Supporting

Necessary and business-specific, but no differentiator. Losing it hurts; having it doesn't win.

Build in-house, modestly. Don't gold-plate.

Generic

A solved problem everyone has: auth, payments, notifications, search, booking.

Buy or adopt. Hand-modeling this is classic waste.

The questions that settle it

Traps: high-traffic ≠ core (a checkout wrapping Stripe is generic). Mission-critical ≠ core (payments are table stakes). The edge is often the quiet piece — a matching rule, a pricing policy.

03

Draw the bounded contexts

A bounded context is the boundary inside which a model and its ubiquitous language stay consistent — every key term has exactly one meaning. The same real-world thing legitimately gets a different model in each context.

SALES Customer = prospect · pipeline · quota "order" → a quote accepted SUPPORT Customer = tickets · SLA · history "order" → a thing to refund DealClosed event + ACL
Same word, two meanings ⇒ two contexts. Integrate with an explicit event, don't share the table.

The boundary signals, strongest first

  1. Linguistic shift. A term changes meaning across the flow ("lead" becomes an "appointment") — or two teams use different words for one thing. The single strongest signal.
  2. Cohesion of change. Things that change together, for the same business reason, belong together. Different reasons to change ⇒ different contexts.
  3. Team ownership. One team must own a context end-to-end. Draw boundaries you'd be happy to see in the org chart.
  4. Business capability. "Fulfillment", not "the validation layer". Capabilities are stable; technical layers aren't boundaries.
  5. Data ownership. Each context owns the writes for its concepts. Shared mutable tables are a smell.

Pressure-test any proposed boundary

When unsure Prefer fewer, coarser contexts early; split once you see the fault lines. A wrong merge is cheaper to fix than a wrong split.
04

Map how contexts relate

Integration patterns follow the relationship between the teams, not the technology. Pick by power dynamic and how much you must protect your model:

Anti-corruption layer
Translate at the border when their model must not leak into yours. Default when consuming anything you don't control.
Open-host + published language
You serve many consumers: expose one stable, documented model.
Customer–supplier
Upstream plans around downstream's needs; you have influence.
Conformist
You can't influence upstream and choose to accept their model as-is.
Shared kernel
Two teams co-own a small overlapping model and coordinate tightly. Rare; keep it tiny.
Separate ways
Integration costs more than duplication — don't integrate.
05

Model inside a context

Tactical patterns only pay off once boundaries are roughly right — and mostly in core subdomains. A CRUD module in a supporting subdomain is not a failure of DDD; it's DDD working.

Value ObjectMoney(amount, currency)

Defined by attributes, immutable, no identity. Two with equal attributes are interchangeable.

Default to these. They kill primitive obsession and pull validation out of entities.

EntityOrder #4821

Identity persists through state changes; equality is by id, not attributes.

Use one only when continuity of identity matters to the business.

Aggregateroot = entry point

A cluster of entities + values changed as one unit. It is the transaction boundary: invariants true at every commit.

Outside code touches only the root, and holds other aggregates by id.

Domain EventOrderPlaced

A past-tense record that something meaningful happened. The tool for decoupling aggregates and contexts.

Cross-aggregate consistency = event + eventual consistency, not a bigger transaction.

Repositoryper aggregate root

Collection-like persistence for aggregates. One per root — not one per table.

Read-models and queries are a separate concern (often CQRS).

Domain Serviceuse sparingly

Stateless logic that truly spans aggregates (a transfer, a cross-aggregate price).

First try the aggregate. Services are not a behavior dumping ground.
06

Aggregate rules of thumb

  1. Model true invariants only. Inside one aggregate goes only what must hold transactionally. "Eventually consistent is fine" ⇒ it doesn't belong inside.
  2. Keep them small. Many small aggregates beat a few large ones — big ones mean locking, contention, and fake invariants.
  3. Reference by identity. Hold customerId, never the Customer object.
  4. One transaction, one aggregate. Need to change two at once? The boundary is wrong — or the second change is an event handler's job.
  5. Events carry consistency across boundaries. A commits, publishes; a handler updates B in its own transaction.
07

Smells to catch in review

Anemic model
Entities are getter/setter bags; behavior lives in "service" classes. Move invariant-protecting behavior onto the aggregate.
God aggregate
"The User" with everything hanging off it. Split along real invariants; reference by id.
Primitive obsession
String email, BigDecimal amount everywhere. Reach for EmailAddress, Money.
Needless identity
An Address with a surrogate id it never semantically needs — that's a value object.
ORM wagging the dog
Lazy-loading and annotations shaping the aggregate. The model must be expressible without the database.
Technical "contexts"
A "validation context", an "API context" — layers aren't boundaries; capabilities are.
08

The field card

Four questions, four answers
Entity or value object?
"Replaced by an identical copy — same thing to the business?" Yes ⇒ value object. It has continuity of its own ⇒ entity.
How big is the aggregate?
"What must be true the instant this transaction commits?" Include only that; connect the rest via id + events.
Where does this behavior go?
On the aggregate that owns the invariant it protects. Only if it genuinely spans several ⇒ a domain service.
Where does this feature go?
In the context whose decision it expresses, in whose language it's phrased, whose data it writes — never "wherever that entity's table lives".