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


