Designing DDD aggregates

Albert Llousas
10 min readOct 26, 2022

The aim of software engineering is to solve business problems, for that we code collections of data structures, relationships within them, abstractions and functions that fit into larger systems.

But software systems are not static, they tend to grow leading us to complex solutions that need to be maintained and evolve.

Domain-Driven Design is a technique composed by a set of practices and patterns that help mitigate this complexity. This article is focused on one of its tactics: the aggregate building block.

The aggregate building block

Domain-Driven Design (DDD) tries to avoid complex graphs of entities connected one to another, a software anti-pattern also known as a big ball of mud.

Instead, it empowers engineers to find a minimal logical set of entities that belong together and that can be treated as a single unit in order to ensure the business rules around them.

These groups of entities are called aggregates.

But, let’s go step by step, concept by concept, to get a good understanding about them.

The use-case: Online Movie Ticketing System

Imagine that we have to design a simple online movie ticketing system, by which:

  • Multiple seats can be booked in a single booking
  • A seat can not be booked twice, we don’t want customers sitting on each other laps
  • All seats within a booking will be for the same movie session, hence same cinema and same screen

Invariants

Now, is time to explain one of the most DDD important concepts when it comes to aggregate design, the invariants ( also known as business invariants or true invariants ):

> An invariant is a business rule that must always be consistent.

> Vaughn Vernon, Implementing Domain Driven Design, p.355

This simple definition give us a strong statement, no matter what happens in our system, an invariant is a business rule that will always hold, it can not be broken, therefore:

  • Models can not be created if invariants are not ensured
  • Models can not be changed if any invariant is broken
  • Consistency must be preserved in a concurrent environment
Note: Fail or success can vary depending on the concurrency strategy

But, how is this related to the aggregates?

The main responsibility of an aggregate is to enforce the consistency of invariants across all the entities within itself.

Maybe at this point you think:

> Ok, I am already doing this without noticing, I call these rules validations ;-).

Usually, validations are checking the state after changes, like a post checks, invariants are a kind of validations that perform checks before the change happens, even before an object is created, they can be seen as pre checks.

Since we already know what an invariant is, let’s come back to our example, we need to know which are the invariants that we will deal with:

These rules are important, they will bias the design of our aggregates.

> Cool, then, are we ready to design our aggregates?

Not yet, first we need to see who and how should ensure the consistency of these business rules.

Transactional-Consistency boundaries

Once we have understood what an invariant is, it is time to introduce the transactional or consistency boundary, yes, a new DDD term, but an important one.

The consistency boundary defines what is the scope of an aggregate, what is inside and what is outside; therefore the boundary defines the aggregate itself. The aggregate must ensure that the model within the boundaries complies with the invariants, no matter what operations are performed.

Defining the boundaries of an aggregate is the same as sizing it, consequently, part of the design process.

These boundaries should not be set in stone, they may, and most likely change. Teams work in an iterative approach, starting with a small set of features, getting feedback and implementing new features or improving existing ones. While the product evolves, changes will impact the invariants, forcing the aggregate boundaries to grow or shrink over time.

> But, why is it called transactional-boundary?

Aggregates have a lot in common with Database Transactions, given a requested operation, such as book seats, they will perform changes inside their boundaries, modifying the models within. Same as a DB transaction, changes should succeed together or fail together and it is the responsibility of the aggregate to ensure consistency and atomicity of them.

When it comes to aggregate design these similarities with transactions brings a few rules to consider:

  • An aggregate must not change anything outside its boundary, if it does, it will not be able to guarantee atomicity and consistency.
  • Since they cannot change external models, aggregates should have as few external relationships as possible, if any, they should be identifiers.
  • A logical operation, such as booking seats for a movie, can change an aggregate multiple times but not different aggregate instances. Changing multiple aggregates could mean that your aggregate is not large enough to enforce the business rules of a given use case.

Maybe now that we have an idea about what a consistency boundary is, we could try to draw some of them in our use-case; here some potential candidates to be aggregates:

- The whole cinema system, including all cinemas worldwide

- A single cinema, with all screens and movies projected all time

- A screen within a cinema

- A seat in a cinema screen, containing all bookings over time

- A movie session

- A booking, of multiple seats

- A single seat booking for a single session

- Many others

Potential consistency boundaries

Ok, how can we decide which one would be a good consistency boundary?

Sizing aggregates

Having an understanding about how important invariants and their transactional boundaries are leaves us in a good position to start with the design.

But, how can we choose a good one? Here a diagram that can help us a bit when it comes to sizing:

Potential problems regarding the size:

  • Contention issues: As they become bigger, they will be more prone to have concurrency issues, which happens when the demand for a shared resource exceeds the supply. Basically, when several requests are racing to use the same aggregate, one of them will win and the rest will have to wait or fail.
  • Performance issues: Aggregates are the basic element of transfer of data storage, they will be saved and fetch them back from datastores, meaning that we will have to transport them; big objects can decrease the overall network performance of the system in terms of latency (response time), throughput (amount of data transmitted) and bandwidth (concurrent data)
  • Scalability: Even though we have to stick to YAGNI principle and solve our immediate needs, we also have to think in multiple growth factors, by which the aggregates could end up holding thousands or even millions of models within, blowing up the performance of the system. Small aggregates tend to adapt and scale better.
  • Consistency issues: Too small aggregates could have issues enforcing the consistency of their invariants and atomicity of the changes since they won’t have all the context within them;
Note: These issues can be solved by introducing additional states and eventual consistency.
  • Simplicity issues: Too granular aggregates can become too restrictive, forcing the introduction of multiple aggregates, communication within them, temporary states and eventual consistency in order to ensure integrity of the invariants. Paradoxically, too small aggregates could increase the overall complexity of the solution.
Note: Even though async events should be the way to communicate between different bounded contexts or microservices most of the time, async communications between different aggregates in the same bounded context to fulfil a single use case could be a signal of excessive granularity.

It is clear, we don’t want big aggregates, downsides can be really painful and difficult to tackle, instead, we prefer small aggregates, they are more flexible in all ways, but large enough to enforce the invariants, both in local operations and in the global scope of the application.

Now we can finally design our cinema booking domain; if we map our candidates, we will be able to assess how good our potential candidates can be:

Good!

We can discard some of them from the scene:

  • The whole cinema system: clearly, it would suffer from all issues of being a large aggregate.
  • A single cinema, a screen within a cinema and a seat in a cinema screen would mainly suffer from scalability perspective. If we keep all the bookings of films projected during all time, in some months the aggregates won’t scale at all.
  • Single seat booking: Too small, ensuring invariants with this candidate would be complicated.

It give us 2 good candidates:

  • Movie session (with all bookings for a single movie session)
  • Booking (multiple seats)

Booking aggregate has a downside: Even though we can pass to the create booking function all the current bookings to ensure that we don’t book seats twice, it will be more complicated to ensure global consistency, if we have the two booking requests in the system at the same time with repeated seats, the invariant can be bypassed.

This can be solved adding additional states (booking pending), async communications and confirmations with other aggregates (eg. movie session or seat per session) but it would increase the complexity of the solution.

Finally, we have a winner!

As we can see in the diagram, the movie session is right in the middle, minimising all the potential issues, so we can consider it as the best candidate!

But this shouldn’t be set in stone, there are several factors that could change it:

  • Listening to the business, if a movie session is not in their vocabulary (ubiquitous language) and they only use booking as a term we will need to assess the tradeoffs and maybe reconsider it.
  • The team and infrastructure are already used to async communications: If we have everything in place to communicate between aggregates, the complexity is not going to increase a lot and we can have a smaller aggregate, reducing scalability, consistency and performance issues.
  • New invariants can bring new growth factors and smaller aggregates will adapt better.
  • When we are ok breaking any rule regarding consistency boundaries or potential issues.

Show me the code!

Here you can find how this solution looks like in terms of code.

Some technical implications

Although DDD tries to be as agnostic as possible from technical aspects, how we design and model the aggregates will have technical implications:

  • We will need to save and fetch our aggregates in datastores that support some level of transactionality in order to enforce global consistency.
  • We will need to lock our aggregates to prevent conflicts, a locking strategy would help with that, either an optimistic one or a pessimistic one.
  • We will need to isolate our aggregates from technical aspects, using architectures such as hexagonal could help a lot.
  • Our aggregates will need to notify of their changes to other bounded contexts, more likely through domain events (event driven to the rescue).
  • Sometimes, we will have invariants that can not be ensured within a single aggregate, business rules that spans multiple aggregates, such as an email address that can not repeated across different User aggregates. For these cases we will need to use external mechanisms such as using a unique database constraint or eventual consistency. (thanks Younes Zeriahi to point that out)

Closing

If you’re questioning why you would need all these additional concerns and naming, try to remember the last time you started working on a system already in place and thought their interdependencies were a nightmare.

Most likely, this system started with a simple idea, but evolved into a tangled system, difficult to maintain and evolve, a big ball of mud.

If it happened to you, maybe you could start looking at or revise this powerful pattern that domain-driven design brings us.

Resources

- Martin Fowler’s — aggregates and datastores

- Vaughn Vernon’s — Effective aggregates

- Vaughn Vernon’s — DDD distilled

- Vaughn Vernon’s — Implementing DDD

- Erik Evans — The Blue book

- Nick Tune blog

--

--