Module 93: Domain-Driven Design

Lesson 4 of 5 · 15–25 minutes

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

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;
  }
}

🖱️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.

← PreviousEntities and value objects
ProLearners Assist