InterviewKata
Software architecture · Level 2: Structure · Module 08b of 46

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

A bank's safe-deposit box. Everything inside it is changed together, under one key, by one person at a time, and the bank guarantees what's in it. To refer to something in another box you write down that box's number; you never reach into two boxes at once. An aggregate is a box: one key (its root), one rule-set it guarantees, and other boxes referenced only by number.

The building blocks

BlockWhat it isExample
EntityAn object with an identity that lasts while its attributes changeOrder #981: still the same order after its address changes
Value objectDefined only by its values; immutable; compared by valueMoney(42.00, GBP), an Address, a DateRange
AggregateA cluster of entities and values changed as one unit, through one root entityOrder (root) with its OrderLines
Domain eventA record that something the business cares about happenedOrderPlaced, PaymentFailed
RepositoryLoads and saves whole aggregates, one repository per aggregate typeorders.get(981), orders.save(order)
Domain serviceA rule that belongs to no single entityPricing 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:

  1. 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).
  2. Design small aggregates. Large ones lock more, load more and collide more often.
  3. Reference other aggregates by identity. An Order holds a customerId, not a Customer object.
  4. 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

Two-sentence answers
  • "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.
Red flags interviewers notice
  • One transaction that updates an order, the customer and the product stock together.
  • Aggregates holding object references to other aggregates.
  • Money as a double and email addresses as raw strings.

6. Remember it

Memory hook: "One box, one key, numbers for the rest"An aggregate is changed in one transaction through its root; everything outside it is referenced by ID and updated by events.
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 bankTactical DDD
A deposit boxAn aggregate
Its keyThe aggregate root
"See box 4471"A reference by ID
A note to the clerk to update box 4471A domain event

The trap it saves you from

Making one giant aggregate "for consistency" that every user collides on.

7. Common mistakes

Aggregates as table joinsModelling an aggregate as "everything that has a foreign key to Order". The boundary comes from invariants, not from the schema.
Bypassing the rootInserting order lines straight into their table skips the root's rules. Lines are only reached through order.addLine().
Calling slow systems inside the transactionA payment call in the middle of saving an order holds locks for hundreds of milliseconds. Save, publish an event, call payment afterwards.
Anaemic entitiesGetters and setters only, with rules in services. The aggregate then can't protect anything.

8. Design exercise (30 minutes)

  1. 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".
  2. Draw the boxes (10 min). For each rule, what must change in one transaction? Draw the smallest aggregates that keep every rule true.
  3. 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?

Defined by its values, immutable, compared by value.

How should an Order aggregate refer to its Customer?

Vernon's rule 3: reference other aggregates by identity.

Product stock is put inside the Order aggregate. What happens during a flash sale?

All those transactions write the same stock record, so most fail and retry.

Two users change the same aggregate at once. What does optimistic concurrency do?

The second save expected an old version; reloading lets the root re-check its rules.

What should a single transaction do, according to the aggregate rules?

Rule 4: eventual consistency outside the boundary.

10. Go deeper

ResourceTimeWhy
Effective Aggregate Design (Vaughn Vernon)1 hourThe three papers behind the four aggregate rules.
DDD Aggregate (Martin Fowler)5 minA short definition with the root and the boundary.
Value Object (Martin Fowler)10 minWhy immutability and value equality matter.
DDD Reference (Eric Evans)referenceDefinitions of every tactical pattern.