Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Aggregates: Consistency Boundaries for Related Objects

Recommended resources

Hello! Welcome back to our journey into Domain-Driven Design.

Introduction

In our previous lessons, we assembled the basic building blocks of our domain model: Value Objects, which describe aspects of the domain, and Entities, which represent objects with a unique identity and lifecycle.

We saw how an Entity like a Customer can change its state over time while its identity remains stable. However, many real-world business operations are more complex. Consider allocating an order line to a batch of stock. This operation involves at least two objects: an OrderLine and a Batch. We must ensure that the Batch's available quantity is reduced if and only if the OrderLine is successfully allocated. We can't have one change without the other. This requirement for consistency across multiple objects is a central challenge in software design.

Today, we'll address this challenge by introducing one of the most important tactical patterns in DDD. Our learning outcome is to: Define an Aggregate as a consistency boundary for a group of related objects.

This lesson will cover:

  • What an Aggregate is and why it's more than just a collection of objects.
  • The concept of "invariants" and why they are critical to your domain.
  • How an Aggregate acts as a "consistency boundary" to protect these invariants, especially in concurrent systems.

1. What Is an Aggregate?

At its simplest, an Aggregate is a cluster of associated domain objects (Entities and Value Objects) that we can treat as a single unit. The key phrase here is "treat as a single unit." Instead of interacting with individual parts, we interact with the Aggregate as a whole.

This structure allows us to manage complexity. From the outside, we don't need to know about the internal details of the Aggregate, only that it represents a cohesive concept from our domain.

The following short video provides a great introduction to the concept.

DDD Building Blocks

The video 'DDD Building Blocks' by Drawing Boxes offers a clear and concise explanation of what an Aggregate is and introduces the related concept of an Aggregate Root.

Please watch the section from 02:54 to 04:20. Focus on the car analogy and the explanation of why an Aggregate is considered a 'consistency boundary' where all business rules must be valid.

As the video explains, the Aggregate defines a boundary. Everything inside this boundary must always be consistent according to the business rules. To formalize this, let's look at a textual definition and explore what an Aggregate is not, to avoid common misunderstandings.

Domain-Driven Design Handbook

The 'Domain-Driven Design Handbook' provides a precise definition of an Aggregate and a powerful example of why it's necessary.

Read the section titled 'Aggregate'. Pay close attention to the list of what an aggregate is not, and the chat application example (liking a deleted message), which perfectly illustrates a consistency problem that Aggregates solve.

The key idea is that an Aggregate is a transactional consistency boundary. This is the most important concept to grasp. Let's break down what that means.


2. The Aggregate as a Consistency Boundary

To understand the Aggregate's role, we first need to understand invariants.

An invariant is a business rule that must always be true at the end of any operation (or transaction). It's a condition that must not be violated.

Consider these examples from an e-commerce domain:

  • An order's total price must equal the sum of its line item prices.
  • The quantity of items in a customer's shopping cart cannot be negative.
  • Stock for a product cannot be allocated beyond the quantity on hand (i.e., we can't oversell).

In a simple, single-threaded application, enforcing these invariants might seem straightforward. But what happens when multiple users or processes try to perform actions at the same time? This is where the concept of a consistency boundary becomes critical.

The video below explains why we need Aggregates to guarantee consistency and maintain these invariants.

How to design great Aggregate Roots in Domain-Driven Design

In 'How to design great Aggregate Roots in Domain-Driven Design', Milan Jovanović explains the fundamental 'why' behind Aggregates.

Watch the first part of the video, from 00:10 to 02:50. The speaker does an excellent job of framing the problem that Aggregates solve: guaranteeing the consistency of changes across a group of objects and enforcing invariants.

The Aggregate, therefore, draws a line around a set of objects. Any operation on any object within that boundary is treated as a single, atomic transaction. If the transaction completes, all invariants inside the boundary must be satisfied. If it fails for any reason, the entire operation is rolled back, and the Aggregate is left in its previous valid state. Nothing is partially updated.

This visual shows how Aggregates create these boundaries within the larger Bounded Context.

This diagram illustrates how a Bounded Context contains multiple Aggregates. Each Aggregate, centered around its Aggregate Root, forms a distinct consistency boundary (the red dashed line) that encapsulates its internal Entities and Value Objects.

The following diagram provides a more detailed look inside two different Aggregates, Buyer and Order.

This diagram shows two separate Aggregates. The `Order` Aggregate is a consistency boundary that includes the `Order` entity (the root), `OrderItem` child entities, and an `Address` value object. Any change, such as adding an `OrderItem`, must be processed through the `Order` aggregate to ensure all rules (invariants) within this boundary are maintained.

3. Identifying and Defining an Aggregate

So, how do we decide what goes into an Aggregate? The primary driver is transactional consistency. You should ask: "What objects must be consistent with each other for a business operation to be valid?"

If changing object A requires an immediate, consistent update to object B, they are strong candidates for being in the same Aggregate. If object B can be updated later (i.e., it can be "eventually consistent"), it should probably be in a different Aggregate.

The goal is to make Aggregates as small as possible. Large Aggregates can lead to performance problems (locking too much of the database) and concurrency conflicts.

The authors of "Cosmic Python" walk through this exact thought process.

7. Aggregates and Consistency Boundaries

The book 'Cosmic Python' provides a fantastic, practical discussion on choosing and defining an Aggregate. It directly addresses the tension between consistency and performance.

Please read the following sections from Chapter 7: Start at 'Invariants, Constraints, and Consistency' and read to the end of 'Invariants, Concurrency, and Locks'. This sets up the problem. Read 'What Is an Aggregate?'. This provides a great definition and the shopping cart example. Read 'Choosing an Aggregate'. This section is crucial as it shows the thought process of grouping Batch objects under a new Product Aggregate to solve a specific consistency problem. Focus on how the need to protect an invariant (not overselling stock) in a concurrent environment leads directly to the creation of the Product Aggregate.

Let's look at the simple Python implementation of the Product Aggregate from the Cosmic Python book. This demonstrates how the logic that was previously in a separate service function is now encapsulated within the Aggregate itself.

from typing import List
from datetime import date

# Assume OrderLine and Batch are defined as they were in previous lessons.
# For context, their structure is roughly:
# class OrderLine:
#     def __init__(self, orderid: str, sku: str, qty: int): ...
#
# class Batch:
#     def __init__(self, ref: str, sku: str, qty: int, eta: date | None): ...
#     def allocate(self, line: OrderLine): ...
#     def can_allocate(self, line: OrderLine) -> bool: ...

class OutOfStock(Exception):
    pass

class Product:
    """This is our Aggregate."""
    def __init__(self, sku: str, batches: List['Batch']):
        self.sku = sku
        self.batches = batches # Contains other domain objects

    def allocate(self, line: 'OrderLine') -> str:
        """
        This method encapsulates the business logic for allocation.
        It ensures the invariant (don't oversell) is protected
        for this Product's set of batches.
        """
        try:
            # The logic for choosing a batch is inside the Aggregate
            batch = next(
                b for b in sorted(self.batches) if b.can_allocate(line)
            )
            batch.allocate(line)
            return batch.reference
        except StopIteration:
            raise OutOfStock(f"Out of stock for sku {line.sku}")

In this example, the Product class is the Aggregate. It groups a list of Batch objects. The allocate logic, which needs to ensure consistency across these batches (e.g., by picking the earliest one with available stock), is now a method on Product. Any external code that wants to perform an allocation will now do so by loading a Product instance and calling its allocate method. The Product Aggregate is responsible for maintaining its own internal consistency.


Conclusion

In this lesson, we've defined the Aggregate pattern and its fundamental role in Domain-Driven Design.

Key Takeaways:

  • An Aggregate is a cluster of related Entities and Value Objects that is treated as a single unit.
  • Its primary purpose is to serve as a consistency boundary.
  • An invariant is a business rule that must always be true after any operation is complete. Aggregates are responsible for enforcing their invariants.
  • All changes to objects within an Aggregate's boundary must happen in a single, atomic transaction. This guarantees that the Aggregate is always in a valid state.
  • The choice of what to include in an Aggregate is driven by the need for true transactional consistency.

Preview of the Next Lesson:

We've established that an Aggregate is a protected boundary. But how do we enforce this protection? How do we ensure that external code doesn't reach inside the boundary and modify internal objects directly, potentially violating the invariants?

In our next lesson, we will answer this by exploring the Aggregate Root. We will learn how to designate one entity within the Aggregate as the single entry point for all commands and modifications, effectively making it the guardian of the Aggregate's consistency.