Module 93: Domain-Driven Design
Aggregates
🏠The everyday picture
An order at a restaurant: the order and its line items belong together. You don't change one item on someone else's bill directly; you go through the order, which makes sure the total and the rules (no more than two specials per table) stay right.
📘What it really is
-
An aggregate is a cluster of entities and value objects treated as one unit for changes, with one entry point, the aggregate root.
-
The root protects the rules (invariants) that must always be true inside the aggregate. All changes go through the root's methods, never by editing the parts directly.
-
In the booking platform, a
TutorScheduleaggregate might own a tutor's availability rules and booked slots, guaranteeing no two bookings overlap:
export class TutorSchedule {
readonly tutorId: string;
private bookings: Booking[];
constructor(tutorId: string, bookings: Booking[]) {
this.tutorId = tutorId;
this.bookings = bookings;
}
book(studentId: string, range: TimeRange): Booking {
if (!this.isAvailable(range)) throw new SlotUnavailableError(); // within the tutor's availability rules
if (this.bookings.some(b => b.isActive() && b.range.overlaps(range))) throw new SlotUnavailableError();
const booking = Booking.request(studentId, range);
this.bookings.push(booking);
return booking;
}
}
- Rules for aggregates:
- keep them small: include only what's needed to protect an invariant (a tutor's schedule for the relevant period, not every booking ever);
- one transaction changes one aggregate: save the whole aggregate together (Module 56); rules that span aggregates are handled by events (Lesson 5) and eventual consistency (Module 94);
- refer to other aggregates by ID, not by holding them (a booking holds a
studentId, not the whole student); - protect against concurrent changes with a version number (optimistic locking) or a database constraint, so two simultaneous bookings can't both succeed (Module 91's ADR).
- Repositories (Module 92) load and save whole aggregates.
🖱️Try it
Implement the TutorSchedule aggregate with book, cancel and reschedule methods enforcing: no overlaps, no bookings in the
past, cancellations allowed only up to 12 hours before (otherwise a late cancellation). Save it through a repository using a version
column and UPDATE ... WHERE version = $n. Write a test where two bookings for the same slot are attempted at once, and show only
one succeeds.
✅Quick check
What does an aggregate root do?
Show the answer
It's the only entry point for changes to the aggregate, and it enforces the rules that must always hold inside it.
Why should one transaction change only one aggregate?
Show the answer
It keeps transactions small and fast; rules across aggregates are handled through events and eventual consistency.
