liveBook · Manning

Aggregate root

As explained in Simple Object-Oriented Design: Create clean, maintainable applications

An aggregate root is the designated domain object through which an aggregate's members and consistency rules are accessed and modified.

An aggregate root is the public domain object for a cluster of related objects. Route operations through the root so it can preserve invariants across the whole object tree; clients hold references to the root rather than its internal objects. section 3.5 section 3.7

This is more than hiding a collection. The root owns consistency rules for domain relationships and is the unit passed to persistence: use one repository or data access object per aggregate root, not one per internal database entity. Direct deletion of an internal object can otherwise leave root-level state inaccurate. section 3.5

Offering controls enrollment changes

Make Offering the root and Enrollment an internal object. Offering.enroll creates an enrollment and decreases available spots; Offering.cancel finds and cancels an enrollment, then restores a spot. A client that cancels Enrollment directly could forget the capacity update. section 3.5.2

Listing 3.10 Offering as an aggregate root
class Offering {
 
  private int id;
  private Training training;
  private Calendar date;
  private List<Enrollment> enrollments;                      #1
  private int maximumNumberOfAttendees;
  private int availableSpots;
 
  public Offering(
   Training training,
   Calendar date,
   int maximumNumberOfAttendees) {
    // basic constructor
  }
 
  public void enroll(Employee employee) {                    #2
    if(!hasAvailableSpots())
      throw new OfferingIsFullException();
 
    Calendar now = Calendar.getInstance();
    enrollments.add(new Enrollment(employee, now));
    availableSpots--;
  }
 
  public void cancel(Employee employee) {                    #3
    Enrollment enrollmentToCancel = findEnrollmentOf(employee);
    if(enrollmentToCancel == null)
      throw new EmployeeNotEnrolledException();
 
    Calendar now = Calendar.getInstance();
    enrollmentToCancel.cancel(now);
 
    availableSpots++;
  }
 
  private Enrollment findEnrollmentOf(Employee employee) {   #4
    // loops through the list of enrollments and
    // finds the one for that employee
    // ...
  }
}

The listing makes the boundary concrete: public enrollment operations update both the Enrollment collection and Offering’s available-spots invariant. section 3.5.2

Breaking the boundary can be reasonable, but decide deliberately:

  • Compare the immediate benefit of direct access with the maintenance cost of stale invariants. section 3.5.1
  • If no invariant spans the objects, split the aggregate into smaller parts instead. section 3.5.1
  • When separate aggregates must coordinate, accept eventual consistency only knowingly and use domain events to notify the affected aggregate. section 3.5.1

How Simple Object-Oriented Design: Create clean, maintainable applications covers it

How deep it goes
Defines access and repository rules, then works through enrollment and cancellation updates while deferring deeper aggregate design.
Examples
Uses Basket with Items and Products, then Offering with Enrollment, available spots, enrollment, and cancellation.
What you can do after
Apply it by routing aggregate changes and persistence through a root while evaluating when smaller aggregates or eventual consistency fit.
How it connects
Connects aggregate roots with invariants, repositories, domain events, eventual consistency, services, and validation.

Also explained in

Compare how 3 books explain aggregate root →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Simple Object-Oriented Design: Create clean, maintainable applications. The section links point to the parts of the book it draws on.