Aggregates own their invariants, and the anaemic trap
You now know entities. An aggregate is a specific, important kind of entity: a cluster of related objects that you treat as a single unit for the purpose of changes, with one object designated as the aggregate root. The root is the only public entry point. Outside code holds a reference to the root and asks it to do things; it never reaches inside to poke the objects the root guards.
The reason aggregates exist is a single word: invariants. An invariant is a rule that must always be true. “An order line quantity is at least one.” “You can’t revert a decision that was never confirmed.” “The total is the sum of the lines.” The aggregate’s whole job is to own its invariants and make sure no operation can ever leave it in a state where one is broken.
Here is where your Nest instinct collides with DDD, and it has a name. The default shape in a lot of TypeScript backends is a class-validator DTO (public fields plus decorators) and a Service that holds all the behaviour. The DTO is a bag of data; the service does the work. That structure is the anaemic domain model, and in DDD it’s the primary smell.
“Anaemic” because the domain object has been drained of behaviour. It’s just fields. All the blood (the rules, the decisions) lives in a service next to the object, operating on it from outside. The problem isn’t that it doesn’t work, it’s that the object can’t protect itself. Any caller that gets a reference can set any field to anything, and the rule that says they shouldn’t lives in a service they might not go through.
The fix is the move you just did in the exercise: fold the behaviour back onto the object. Instead of OrderService.cancel(order) reaching in to check and set order.status, give Order a private status and a cancel() method that checks its own state and transitions itself. Now the rule lives on the thing it’s a rule about, no external caller can set the status directly, and the order literally cannot be left in a state its own rules forbid. The service shrinks to orchestration: fetch the order, call order.cancel(), save.
There’s a sharper version of this trap that Fusion review calls out by name, and it’s worth pre-loading now because you’ll meet the real comment in the final module. It’s tempting, when the object needs some fact to make a decision, to pre-compute that fact in the calling layer and pass it in: the caller queries the database, distils it to a boolean like hasActiveDecision, and hands that boolean to the aggregate’s method. That feels helpful. It’s the anaemic trap in disguise.
Why? Because now the aggregate isn’t answering from its own state, it’s validating a boolean the caller gathered. It can’t answer questions about itself. The invariant has leaked back out into the caller (and often into a database WHERE clause too, so the same rule now lives in two or three places). The DDD-correct move is: load the aggregate whole, with the state it needs, then ask it to act. It decides from what it holds. The rule stays in exactly one place, on the aggregate.
That’s the concept. Fusion has a precise mechanical expression of all of it: action methods that return a Result, a base Entity that forbids accidental value-equality, and a hard rule against passing pre-computed booleans in. You’ll see the mechanics in Module 4 and the real review comments in the final module. For now, hold the shape: aggregate owns its rules, behaviour lives on the object, never drain it into a service.
Try it yourself
Define an aggregate and an invariant
Two words that do a lot of work in DDD.
What is an aggregate, and what is an invariant?
Reveal answer
An aggregate is a cluster of related objects treated as one unit for changes, with one object (the aggregate root) as the single entry point: outside code talks to the root, never to its insides. An invariant is a rule that must always hold true for the aggregate (e.g. 'you can't revert a decision that was never confirmed'). The aggregate's job is to own and enforce its invariants, so no code path can leave it in a broken state.
Spot the anaemic model
Which of these is the anaemic-domain-model smell DDD warns against?
Show answer
Correct answer: A — A class of public fields with no behaviour, plus a separate Service that holds all the logic
Public fields with no behaviour plus a logic-holding Service is the anaemic domain model: the object is a data bag and the domain logic has leaked out into a service that operates on it. It's the default Nest shape (DTO + service) and DDD's primary structural smell. The other three are the healthy shapes.
Fold service logic onto the object
Do the move that fixes an anaemic model. Scratch file or paper.
- Start with the Nest shape: a
class Order { items: Item[]; status: string }(public fields) and anOrderService.cancel(order)that checksif (order.status !== 'placed') throw ...then setsorder.status = 'cancelled'. - Ask: where does the rule 'only a placed order can be cancelled' live? (In the service, outside the object. Any caller that mutates status directly can break it.)
- Move it: give Order a private status and a method
cancel()that checks its own state and transitions itself. Delete the service's cancel logic. - Confirm the rule now lives ON the object, no external caller can set status directly, and the object can't be left in a state its own rules forbid
Don't pre-compute state for the object
A specific version of the anaemic trap that Fusion review names directly.
Why is it a smell for the calling layer to pre-compute booleans like hasActiveDecision and pass them into an aggregate's method, rather than letting the aggregate answer from its own loaded state?
Reveal answer
Because it turns the aggregate into a stateless validator over data the caller gathered: the aggregate can no longer answer questions about its own state, and its invariants have leaked out into the caller (and often into database WHERE clauses). The fix is to load the aggregate whole with the state it needs, then ask it to act; it decides from what it holds, and the rule stays in one place.