Skip to content

Unlocking The Power of Aggregates in Domain Driven Design

Updated: at 03:12 PM

In the realm of software engineering, managing complexity is an ongoing challenge, especially as systems scale up and evolve. One modeling tool we can employ for this objective is aggregates. These powerful constructs provide a structured approach to organizing domain objects, enforcing consistency, and encapsulating business logic within bounded contexts.

In this article, we delve into the fundamental concepts of Aggregates, exploring their significance, structure, and role in designing robust domain models. Drawing insights from Eric Evans’ principles and real-world examples, we illuminate the rationale behind Aggregates and their essential contribution to building successful software systems.

Let’s embark on this voyage into the heart of Domain-Driven Design, where Aggregates reign supreme as guardians of consistency, integrity, and coherence within our software systems.

Table of contents

Open Table of contents

Why do we need Aggregates?

The Challenge

It is difficult to guarantee the consistency of changes to objects in a model with complex associations. Invariants need to be maintained that apply to closely related groups of objects, not just discrete objects. Yet cautious locking schemes cause multiple users to interfere pointlessly with each other and make a system unusable.

  • Eric Evans

Since we aim to maintain the success of our systems, we require some modelling techniques to ensure that successful systems do not spiral out of control due to the increasing complexity of models arising from operational scaling and development.

Aggregates are concepts that maps easily to code and help us to keep code cohese and decoupled.

What are Aggregates?

In Domain-Driven Design (DDD), aggregates are clusters of domain objects treated as a single unit for data changes. Each aggregate has a root and a boundary. The boundary defines what is inside the aggregate, while the root is a single specific Entity contained within it.

They are named using an “Ubiquitous Language,” terms and nouns defined by Domain Experts and business people.

Thus, an Aggregate is:

Let’s attempt to identify some Aggregates while modeling a simple E-commerce Order Process:

bounded-context-event-storming-dark.png

In this example, a customer places an order, which then undergoes validation and is eventually confirmed. Subsequently, the Inventory System verifies availability and other rules before redirecting the user to the payment page, and so forth. By tracking the events that can occur during this User Journey, we can identify three bounded contexts:

Unfortunately, before going on talking about aggregates, we need to introduce another concept


Bounded Context

In Domain-Driven Design (DDD), a Bounded Context is a specific area within a software system where a particular model or terminology applies consistently.

Eric Evans, in his book “Domain-Driven Design: Tackling Complexity in the Heart of Software,” provides the following definition:

Bounded Context delimits the applicability of a particular model so that team members have a clear and shared understanding of what has to be consistent and how it relates to other contexts. Within that context, work to keep the model logically unified, but do not worry about applicability outside those bounds.

To elaborate:

  1. Delimits Applicability: A Bounded Context defines the scope within which a particular model, set of concepts, or language is valid and applicable. It helps delineate where certain terms, rules, and concepts hold true and have significance.
  2. Ensures Consistency: Within a Bounded Context, all concepts and terms should be consistent and have a clear, agreed-upon meaning among team members. This consistency aids in communication and understanding within the team.
  3. Defines Relationships: Bounded Contexts also define relationships between different models or concepts. They clarify how the models within the context interact with each other and how they relate to models in other contexts.
  4. Encourages Logical Unity: While each Bounded Context has its own specific model and terminology, it should maintain logical unity within its boundaries. This means that within the context, the model should be cohesive and make sense as a whole.
  5. Acknowledges Differences: Bounded Contexts recognise that different parts of a system may have different models and interpretations of concepts. They allow for flexibility and adaptation to varying requirements and perspectives within different parts of the system.

Overall, Bounded Contexts help manage complexity by providing clear boundaries for models and concepts within a system, enabling teams to work effectively and collaboratively while ensuring consistency and logical coherence within specific areas of the domain.

Without too much approximation, you can consider Bounded Contexts as Aggregates containers.


Ok let’s go back to the example: inside the Order Management Bounded Context we spot the first candidate to be an Aggregate:

aggregate-dark.png

Orders define a Boundary, inside it you can find

Those domains object can be implemented in code as classes and modules, the entire aggregate can be a Microservice, a namespace, a Package, a Class or a Module.

Each Aggregate should be as big as needed as small as possible.

The difference between Entities and Value Object it outside the scope of this post, but you can find it on the internet with a bit of research.

What makes the aggregates a special concept is that it must adhere to the following aggregate rules:

1. Aggregates internals are accessible ONLY through it’s ROOT Entity

References to Aggregate internals from the outer world are strictly forbidden!

You can only access Aggregate internals (entities and value objects) from the inside or by passing through the Entity Root.

To be more clear, Let’s illustrate this with an example where this concept is violated:

// We get an Account aggregate somewhere
Account account = repository.get();

// Reference given to the outer world
AccountToken token = account.getAccountToken();

// Data changes not guarded by the root
token.invalidate();

// Invariant checked outside the aggregate
Challenge challenge = Challenge.fromRequest(request);
if (!account.getToken().confirm(challenge)) {
    throw new Exception("Challenge invalid.");
}

Instead, The preferable approach:

// We get an Account aggregate from the repository
Account account = repository.get();

// Data changes handled by the aggregate root
account.invalidateToken();

// Give out an immutable value instead
ReadOnlyToken readOnlyToken = account.getReadOnlyToken();

// Invariant checked inside the aggregate
Challenge challenge = Challenge.fromRequest(request);
account.confirm(challenge);

// Eventually throws a domain exception

2. Aggregate integrity rules must be ALWAYS granted

An aggregate should represent a cohesive cluster of related domain objects subject to consistency rules and business invariants.

2.1 Integrity granted via Transactional Consistency

Does Every Aggregate need Transactional Consistency?

Not all aggregates necessarily require transactional consistency. Some aggregates may deal with read-only data or data that can be updated independently without strict transactional boundaries.

For example, consider a reporting aggregate in an analytics system that aggregates data from multiple sources for generating reports. This aggregate may not require strict transactional consistency because the data it operates on is primarily read-only or asynchronously updated. It still represents a logical grouping of domain objects for a specific purpose within the domain, even though transactional consistency might not be a primary concern.

In summary, while transactional consistency is often a consideration in designing aggregates, it’s not a defining characteristic. Aggregates are primarily about grouping related domain objects to enforce consistency and encapsulate business logic, and they can exist even when strict transactional consistency is not required.

However, for some aggregates, maintaining data integrity and enforcing business rules are crucial.

How to Ensure Transactional Consistency?

Transactional consistency, for instance, can be enforced at the database level having ACID transaction:

2.2 Integrity granted via Business Invariant

An invariant, in the context of Domain-Driven Design (DDD), refers to a condition or rule that must always be true within a specific domain model or within an aggregate. Invariants define the constraints that govern the valid state of the domain objects. These invariants are essential for maintaining data integrity and ensuring that the domain model adheres to the business rules.

Let’s consider an example of an aggregate called Order in an e-commerce domain.

public class Order {
    private String orderId;
    private List<OrderItem> orderItems;
    private boolean isPaid;

    // Constructor, getters, setters, and other methods

    public void addItem(OrderItem item) {
        // Business rules validation
        if (!isPaid) {
            orderItems.add(item);
        } else {
            throw new IllegalStateException("Cannot add items to a paid order.");
        }
    }

    public void markAsPaid() {
        // Business rules validation
        if (!isPaid) {
            isPaid = true;
        } else {
            throw new IllegalStateException("Order is already paid.");
        }
    }
}

public class OrderItem {
    private String productId;
    private int quantity;
    private BigDecimal price;

    // Constructor, getters, setters, and other methods
}

In this example:

  1. Order is the aggregate root, representing an order in the e-commerce system.
  2. OrderItem represents an item within an order.
  3. The Order aggregate enforces the following business invariant via isPaid flag: “Once an order is paid, no further modifications to its items should be allowed.”

Business rule is ensured through the addItem and markAsPaid methods:

Both operations are encapsulated within the Order aggregate, ensuring that the order’s state transitions are consistent within a transaction boundary. This ensures that any changes made to the Order maintain data integrity.

Invariants are crucial for maintaining the consistency and correctness of the domain model, and they play a significant role in designing aggregates and defining business logic.

Now that we understand what an Aggregate is, let’s attempt to identify a second one within the Inventory bounded context.

bounded-context-dark.png

It appears that orders are also received by the Inventory system, but this time, they seem to be slightly different. Even though both aggregates share the same name and depend on each other, utilizing the same value objects and entities, they are indeed two distinct aggregates. In fact, another important rule concerning aggregates is the identity rule:


3. Identity Rule

Two identical aggregates are actually distinct, distinguished by an identity.


The two aggregates are indeed distinct; however, communication between them should be eventually consistent!

As Eric Evans says:

Any rules that spans Aggregates will not be expected to be up-to-date at all times. Through event, processing, batch processing, or other update mechanism, other dependencies can be resolved within some specified time.

Final Thoughts and Recap

In conclusion, Aggregates stand as essential constructs within Domain-Driven Design, providing a structured approach to organizing domain objects, enforcing consistency, and encapsulating business logic. Through our exploration, we’ve gained a deeper understanding of Aggregates and their significance in building robust and scalable software systems.

We began by defining Aggregates as clusters of associated objects treated as a single unit for data changes, with each aggregate having a root entity and a boundary that defines its scope. Understanding Aggregate rules, such as encapsulation and maintaining consistency, is crucial for effective domain modeling.

Furthermore, we delved into practical examples to illustrate the importance of adhering to Aggregate principles, highlighting the pitfalls of violating encapsulation and the benefits of enforcing consistency and business rules within aggregates.

Moreover, we explored the concept of Bounded Contexts and their relationship with Aggregates, emphasizing the need for clear boundaries and cohesive models within specific contexts.

In our journey, we’ve discovered that while Aggregates may share common elements across different contexts, they remain distinct entities with their own identity and purpose. Communication between aggregates should be eventually consistent, ensuring data integrity and coherence within the system.

In essence, Aggregates serve as pillars of strength in the ever-evolving landscape of software engineering, enabling teams to navigate complexity with confidence and clarity. By embracing the principles of Domain-Driven Design and harnessing the power of Aggregates, developers can build resilient, adaptable, and scalable software systems that meet the evolving needs of users and stakeholders alike.

See you in the next post! ;)

References