Aggregates in Domain Driven Design

My motivation for this blog is to share some understanding on aggregates — what they are and why they are important. Having experienced the pain of delving into it for the last few months from 0 understanding of DDD, I hope it would help beginners who would possibly have to go through the same things.

An aggregate is an encapsulation of entities and value objects (domain objects) which conceptually belong together. It also contains a set of operations which those domain objects can be operated on.

As a concrete example, an aggregate might be a Car, where the encapsulated domain objects might be Engine, Wheels, BodyColour and Lights; similarly in the context of manufacturing a car, operations might be: PaintBody, InstallWheel, InstallEngine and InstallLight and Ship. Your business rules might be:

  • A ready to ship car must have exactly 4 wheels
  • No yellow cars are manufactured
  • Lights must be installed after car body is painted
  • A ready to ship car must have exactly 16 lights
  • A ready to ship car must have an engine and a painted body

Without over-critiquing my knowledge on cars and car manufacturing, we have the following aggregate in C#:

public class Car
{
public Engine Engine { get; private set; }
private List<Wheel> _wheels;
public IReadOnlyList<Wheel> Wheels => _wheels;
public string BodyColour { get; private set; }
private List<Light> _lights;
public IReadOnlyList<Light> Lights => _lights;
public bool IsShipped { get; private set; }
    public Car(List<Light> lights, List<Wheel> wheels, string bodyColour, Engine engine, bool isShipped)
{
_lights = lights;
_wheels = wheels;
BodyColour = bodyColour;
Engine = engine;
IsShipped = isShipped;
}
    public void PaintBody(string colour)
{
if (colour == "yellow")
{
throw new ArgumentException("No yellow cars are allowed");
}
if (_lights.Count > 0)
{
throw new ArgumentException("Some lights are already installed");
}
BodyColour = colour;
}

public void InstallLight()
{
if (_lights.Count == 16)
{
throw new ArgumentException("Lights are fully installed");
}
if (BodyColour != null)
{
throw new ArgumentException("Body is already painted");
}
}
    public void InstallEngine(Engine engine)
{
Engine = engine;
}
    public void InstallWheel(Wheel wheel)
{
if (_wheels.Count == 4)
{
throw new ArgumentException("Wheels are already fully installed");
}
_wheels.Add(wheel);
}
     public void Ship()
{
if (_wheels.Count != 4)
{
throw new ArgumentException("Wheels are not fully installed yet");
}
if (_lights.Count != 16)
{
throw new ArgumentException("Lights are not fully installed yet");
}
if (Engine == null)
{
throw new ArgumentException("Engine is not installed yet");
}
if (BodyColour == null)
{
throw new ArgumentException("Body is not painted yet");
}
IsShipped = true;
}
}
Aggregates are the basic element of transfer of data storage — you request to load or save whole aggregates. Transactions should not cross aggregate boundaries.
— Martin Fowler

In this example, when you retrieve a Car object from your persistence layer, you must retrieve its Engine, Lights, Wheels and BodyColour. Similarly, when you save, you must also save those properties.

Why is this pattern important?

Aggregates are fundamentally about defining consistency boundaries and enforcing invariants.

Consistency boundary is the boundary which the aggregate defines. In a bigger context of vehicle manufacturing, Car keeps the rules of manufacturing of cars consistent. Invariants is just a fancy word for rules. It enforces those rules by keeping the Car object consistent. This brings a few benefits:

  • Encourages a top down approach where implementation is dictated by the business requirements
  • Acts as a layer of abstraction, thus allowing the application to be persistent ignorant

In general, a set of well defined aggregates cover the entirety of your persistence layer. This means that there is a single source of truth for what the business rules are, making it easy for:

  • Adaption to business requirement changes
  • Developers new to the project to identify what the project is about

Risks and mitigation

Aggregates are fundamental write models to your persistence layer. It is absolutely crucial to have a well defined set of aggregates. What does a set of well defined aggregates look like?

First of all, invariants in the set of aggregates must be pair-wise mutually exclusive. This means that two aggregates must not have overlapping invariants and entities which may or may not contradict with each other. The consequences of this would be that there would potentially be inconsistency across your data storage.

Secondly, if an aggregate becomes big with many domain objects, generally it would mean retrieving/updating data storage would become less performant, obviously hugely dependent on the type of data storage being used.