Domain-Driven Design: tactical design
Strategic design (Module 08a) draws the borders. Tactical design is what goes inside one bounded context: which objects have an identity, which are just values, and above all where one transaction ends. That last choice, the aggregate boundary, decides whether business rules can ever be broken, how often concurrent users collide, and how a monolith can later be split into services.
1. The idea
The building blocks
| Block | What it is | Example |
|---|---|---|
| Entity | An object with an identity that lasts while its attributes change | Order #981: still the same order after its address changes |
| Value object | Defined only by its values; immutable; compared by value | Money(42.00, GBP), an Address, a DateRange |
| Aggregate | A cluster of entities and values changed as one unit, through one root entity | Order (root) with its OrderLines |
| Domain event | A record that something the business cares about happened | OrderPlaced, PaymentFailed |
| Repository | Loads and saves whole aggregates, one repository per aggregate type | orders.get(981), orders.save(order) |
| Domain service | A rule that belongs to no single entity | Pricing an order using customer tier and promotions |
Value objects remove a surprising number of bugs: a Money type can't be added to a different currency by accident, and an EmailAddress is validated once, when it's created.
Vaughn Vernon's four rules for aggregates
From his "Effective Aggregate Design" papers (2011), still the standard advice:
- Model true invariants in consistency boundaries. Put inside one aggregate only what must be consistent at the end of every transaction (an order's total equals the sum of its lines).
- Design small aggregates. Large ones lock more, load more and collide more often.
- Reference other aggregates by identity. An Order holds a
customerId, not a Customer object. - Use eventual consistency outside the boundary. One transaction changes one aggregate; others react to its domain events.
2. How it works
An aggregate root guarding a rule under concurrency
Rule: an unverified customer's order may not exceed £500. The same customer adds items from a phone and a laptop at the same moment. Every change goes through the Order root, which carries a version number.
Between aggregates: domain events, not one big transaction
Placing an order should reserve stock, but stock belongs to the Inventory aggregate. Rule 4 says: change one aggregate per transaction, and let the other react.
3. Try it yourself
Draw the aggregate boundary
An Order aggregate during a flash sale: a popular product gets 40 orders a second, and a large business customer's 200 buyers place 2 orders a second between them. Choose what goes inside the Order aggregate and watch the cost. Each write's transaction lasts 50 ms, or 800 ms if it waits for the payment provider.
4. Trade-offs
Big vs small aggregates
A big aggregate guarantees more rules at once, and locks, loads and conflicts more. A small one scales, and moves some rules into eventual consistency with compensation.
Immediate vs eventual rules
Ask the business: must stock be correct to the unit at the instant of ordering, or is "reserve now, apologise rarely" acceptable? Usually the second, which permits small aggregates.
Rich model vs simple CRUD
Aggregates, value objects and events pay off in complex core subdomains. For a simple supporting one (Module 08a), plain CRUD is cheaper and clearer.
Object references vs IDs
Object references make navigation easy and quietly load half the database. IDs make loading explicit and let aggregates later live in different services.
5. In real projects
Optimistic concurrency is the usual guard. Store a version number with each aggregate; save with UPDATE … WHERE id = ? AND version = ?. If no row matches, someone else changed it first: reload and retry, or report the conflict. ORMs such as Hibernate (@Version) and Entity Framework (concurrency tokens) do this for you (see Module 17 of System design).
Domain events go out through an outbox. Saving the aggregate and recording its events in the same database transaction, then publishing from that outbox table, avoids losing events or publishing ones that never happened (Module 20).
Aggregates are the seams for splitting later. If aggregates reference each other by ID and talk through events, moving Inventory into its own service changes wiring, not rules. If they share transactions, it means a rewrite and sagas (Module 24).
Say it in the interview
- "What's an aggregate?" A cluster of objects changed together through one root, which enforces their invariants in a single transaction. Other aggregates are referenced by ID and updated through events.
- "How do you stop overselling?" Inventory is its own small aggregate per product; ordering raises an event and Inventory reserves stock in its own transaction, compensating if it runs out. Putting stock inside every order would make every order of a hot product collide.
- "Entity or value object?" If I care which one it is over time, an entity; if two with the same values are interchangeable, a value object, and I make it immutable.
- One transaction that updates an order, the customer and the product stock together.
- Aggregates holding object references to other aggregates.
- Money as a
doubleand email addresses as raw strings.
6. Remember it
How this hook works: what "one box, one key, numbers for the rest" means
What the slogan means, phrase by phrase
- "One box"
- An aggregate: the consistency boundary for its invariants.
- "one key"
- The aggregate root: all changes go through it, one transaction at a time.
- "numbers for the rest"
- Reference other aggregates by identity and reach them through domain events.
Why this picture fits: a deposit box is guaranteed as a whole, opened by one key, and you can't open two at once.
The story → the concept
| The bank | Tactical DDD |
|---|---|
| A deposit box | An aggregate |
| Its key | The aggregate root |
| "See box 4471" | A reference by ID |
| A note to the clerk to update box 4471 | A domain event |
The trap it saves you from
Making one giant aggregate "for consistency" that every user collides on.
7. Common mistakes
order.addLine().8. Design exercise (30 minutes)
- List the rules (10 min). For a booking system, write five business invariants, such as "a room can't be double-booked for a night".
- Draw the boxes (10 min). For each rule, what must change in one transaction? Draw the smallest aggregates that keep every rule true.
- Wire with events (10 min). For each rule that spans aggregates, name the domain event and the compensating action if the second step fails.
9. Self-check quiz
Money(42.00, GBP): two instances with the same amount and currency are interchangeable. What is it?
How should an Order aggregate refer to its Customer?
Product stock is put inside the Order aggregate. What happens during a flash sale?
Two users change the same aggregate at once. What does optimistic concurrency do?
What should a single transaction do, according to the aggregate rules?
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| Effective Aggregate Design (Vaughn Vernon) | 1 hour | The three papers behind the four aggregate rules. |
| DDD Aggregate (Martin Fowler) | 5 min | A short definition with the root and the boundary. |
| Value Object (Martin Fowler) | 10 min | Why immutability and value equality matter. |
| DDD Reference (Eric Evans) | reference | Definitions of every tactical pattern. |