Domain-Driven Design
Model the code on the business, in the business's own words. Know the building blocks (entities, value objects, aggregates, repositories, events) and where to draw the boundaries.
Overview
Domain-Driven Design (DDD) is a way of building software where the shape of the code follows the shape of the business problem. Eric Evans described it in *Domain-Driven Design: Tackling Complexity in the Heart of Software* (Addison-Wesley, 2003). The book has two halves. The strategic half is about drawing boundaries between models in a large system. The tactical half is a set of building blocks for one model: entities, value objects, aggregates, repositories, domain services, domain events and factories.
Low-level design interviews borrow the tactical half heavily. When an interviewer says 'design the classes for an order system', they listen for whether you separate things with identity from things without it, whether you know which objects must change together, and whether business rules live in the domain objects or leak into service classes.
Two halves of DDD
- Strategic — Where does one model end and another begin? — Bounded context, context map, ubiquitous language
- Tactical — What are the objects inside one model? — Entity, value object, aggregate, repository, domain service, domain event, factory
Ubiquitous language
The first DDD rule is about words, not code. The team agrees on one vocabulary with the domain experts and uses it everywhere: in conversation, class names, method names, tests and documents. If the business says a subscription 'lapses', the code has subscription.lapse(), not setStatus(3).
- The order is placed — order.status = 2 — order.place()
- The subscription lapses — sub.active = False — subscription.lapse()
- A customer's credit limit — user.limit — customer.credit_limit
- A shipment is dispatched — updateFlag(ship, 'D') — shipment.dispatch()
Bounded contexts
A large business does not have one meaning for each word. 'Customer' means a buyer with a credit limit to Sales, a ticket owner to Support, and a delivery address to Shipping. Forcing one Customer class to serve all three produces a class nobody understands. DDD instead draws a boundary, a bounded context, around each model. Inside a context a word means one thing. Between contexts, an explicit translation happens.
The map of contexts and how they relate is the context map. Evans names several relationship kinds; the ones that come up in interviews are in the table.
- Shared kernel — Two teams share a small piece of model and change it together
- Customer and supplier — The downstream team's needs shape the upstream team's interface
- Conformist — The downstream team accepts the upstream model as is
- Anticorruption layer — The downstream team wraps the upstream model in a translation layer so its own model stays clean
- Open host service and published language — The upstream team exposes one well-documented protocol for everyone
- Separate ways — The contexts do not integrate at all
The building blocks
Inside one context, DDD gives the objects roles. The diagram is a small order model with one of each.
- Entity — Yes, an id that survives changes — Yes — Order, Customer
- Value object — No, equal when fields are equal — No, replace it — Money, Address, DateRange
- Aggregate — The root entity's id — Only through the root — Order with its lines
- Repository — No — No — OrderRepository.get(id)
- Domain service — No — No — PricingService.quote(order)
- Domain event — No — No — OrderPlaced
- Factory — No — No — Order.start_for(customer)
Aggregates and invariants
An aggregate is a group of objects that must be consistent together. One entity is the root. Outside code holds a reference only to the root and changes the group only through the root's methods, so the root can enforce every rule (the invariants) before the change lands. One aggregate is saved in one transaction; a change that spans two aggregates is done in two steps, usually joined by a domain event.
- Keep aggregates small — A big aggregate is locked and loaded as one unit, so it becomes a contention point
- Reference other aggregates by id, not by object — Loading one aggregate must not drag in another
- One aggregate per transaction — The invariant lives inside one boundary; across boundaries use events and eventual consistency
- Put the rule where the data is — order.add_line() checks the line rule; nothing else can bypass it
What you will be able to do
- Say what DDD is in two sentences and name its two halves.
- Sort the classes in a design into entities, value objects and aggregates, and defend each choice.
- Draw an aggregate boundary and state the invariant it protects.
- Explain a bounded context with a word that means different things to different teams.
- Write a small rich model in Python where the rules live inside the objects.
Interview questions
- What is the difference between an entity and a value object? Show it in code.
- What is an aggregate, and how do you choose its boundary?
- What is a bounded context, and how do two contexts talk to each other?
- What is a repository, and how is it different from putting SQL in the domain class?
- What is a domain event, and why would you use one instead of a direct call?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio