An aggregate is a cluster of related domain objects treated as one consistency boundary. Its aggregate root is the only object clients access: it coordinates changes across the object tree so internal objects cannot be changed without its knowledge. section 3.5
This is more than hiding a list of children. Choose an aggregate when domain relationships carry invariants that must stay correct together. Pass and persist the whole root, and use one repository or data access object per root rather than one per internal database entity. section 3.5
Offering and enrollment
An Offering owns multiple Enrollments. Offering.enroll creates an Enrollment and reduces available spots; Offering.cancel finds and cancels the enrollment, then restores a spot. Direct enrollment cancellation could leave the available-spots count wrong, so clients change enrollment only through Offering. section 3.5.2 section 3.5
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: Offering owns enrollments and exposes enroll and cancel operations that maintain its attendance invariant. section 3.5.2
Apply the boundary pragmatically:
- Question whether a child truly belongs in the aggregate when no invariant requires coordinated updates; if not, split it into smaller parts. section 3.5.1
- Consider eventual consistency and domain events when separate aggregates must be notified of changes. section 3.5.1
- Break the boundary only after weighing a framework, performance, or boilerplate benefit against the maintenance cost and risk to invariants. section 3.5.1


