Aggregates: the epicenter of the domain

An aggregate is an aggregation of domain objects that are tightly related to each other, meaning that there's a boundary around these objects where consistency and transactionality are mandatory.

Category

Domain-Driven Design and Hexagonal Architecture

Related tags

aggregate, ddd, domain-driven design, Demeter's Law

Introduction

An aggregate is an aggregation of domain objects that are tightly related to each other, meaning that there's a boundary around these objects where consistency and transactionality are mandatory.

Entities and Value Objects shaping Aggregates

There are situations where entities and value objects have strong dependencies between them. For example, when we talk about invoices and invoice lines, it's pretty clear we cannot have an invoice line without an invoice; not only that, but the sum of the values of the invoice lines must be equal to the invoice value; moreover, if we change the value of an invoice line, we need the total of the invoice to be updated also. This kind of consistency and transactionality dependencies are the kind of insights you are looking for when you are thinking about aggregates.

So, in this example, we have two entities that depend on one another: invoice and invoice line (you could argue whether an invoice line is an entity or a value object, by the way). To add a little bit more substance, we can consider also a value object: the invoice status; again, it's pretty obvious that we cannot have an invoice status without... an invoice.

So we have the invoice, invoice status, and invoice lines. How do we organize this aggregate?

The aggregate root

First, we need to define one more concept: the aggregate root. The aggregate root is the entity that commands the relationship. The "belongs to" relationship used to work well here: what is the domain object to which the others belong to? In this case, the invoice status and invoice lines belong to the invoice, so it is easy to see that the aggregate root, the root from which the leaves hang, is the invoice entity.

Creating an aggregate

Once we have defined the aggregate root, it's very easy to create the aggregate. Just use the root as the base, and add the other objects as attributes. With some considerations, though:

  • As we have a consistency boundary, we need to ensure all the domain rules are met. And we need to do this inside the aggregate. But, what happens if we create some of the objects outside the aggregate? We would be ignoring the domain rules checks of the aggregate. So, in conclusion, we need to avoid creating those resources outside the aggregate. Some programming languages have features to allow object creation only from other objects. Use it. If your language does not support this, at least enforce this as a team rule.
  • As we have transactionality rules, we need to ensure every change on any of the objects is done from inside the aggregate, and that database changes are going to be transactional to this aggregate.
  • When adding entities to an entity root as part of the aggregate, don't add the whole entity: instead, add an id pointing to it. If not, you'll end up with performance issues sooner or later.

Code example

Let's see a code example in PHP of the aggregate we've been talking about:

<?php

namespace App\InvoiceExample;

class Invoice
{
    private Id $id;
    private InvoiceStatus $invoiceStatus;
    private InvoiceLineCollection $invoiceLineCollection;

    private function __construct(Id $id, InvoiceStatus $invoiceStatus, InvoiceLineCollection $invoiceLineCollection) {
        $this->id = $id;
        $this->invoiceStatus = $invoiceStatus;
        $this->invoiceLineCollection = $invoiceLineCollection;
    }

    public static function create(Id $id): static
    {
        return new static(
            $id,
            InvoiceStatus::createDefault(),
            InvoiceLineCollection::createEmpty()
        );
    }

    public function addInvoiceLine(string $concept, Money $amount): static
    {
        $this->invoiceLineCollection->addLine($concept, $amount);

        return $this;
    }

    public function total(): Money {
        $total = Money::create(0.00);

        foreach($this->invoiceLineCollection->lines() as $line) {
            $total->add($line->amount());
        }

        return $total;
    }

    public function id(): Id
    {
        return $this->id;
    }
}

There are some interesting things to consider:

  • I use a named constructor (create). I find it very useful to privatize the constructor so I can add more semantics to how am I constructing the class. For this example, I've added the create method, but I could add methods like createEmpty, createWithoutVAT, or any other semantic way to create an invoice you may think.
  • The method addInvoiceLine is the only way to add lines to the internal invoiceLineCollection object. We can add as many validations as we want inside the InvoiceLineCollection value object, but also to the addInvoiceLine method to check the internal consistency of the invoice.
  • We are getting the total of the invoice by summing all the invoice lines, meeting the business rule that the invoice total must be equal to the invoice line's total.

Law of Demeter and Tell, don't ask in the context of aggregates

Maybe you already noted this, but by hiding some objects inside an aggregate, we are gracefully applying Demeter's Law. As a reminder, Demeter's law emphasizes the principle of least knowledge, that is, a consumer should know as less as possible from the inside of the consuming object. In our dummy invoice class, consumers of the Invoice object know nothing about InvoiceLine or InvoiceStatus because they are accessing through the API of Invoice. We are applying Demeter's law.

We are also applying the tell, don't ask principle, which states that a consumer should tell a consuming object what to do by sending a command to its API rather than knowing its internal shape, creating or modifying objects, and then saving. And this is exactly what we do by executing the method addInvoiceLine. We don't create an InvoiceLine and add it to a collection but we just tell Invoice to do the necessary stuff to have a new line.

More information

If you want more information about aggregates, I can recommend you some resources:

Not another newsletter

  • A montly summary of everything new in the blog: So you can consume the content at your own pace.
  • Discounts on my books or courses: You will have exclusive discounts on books, courses or any other pay material I build in the future.
  • Books recommendations: I will recommend you books time to time, only the best of the best.

Join to get more like this

Only one email per month, period. Unsubscribe any time.