Domain Driven Design Aggregates in Laravel

Posted on

Lately, I’ve been focusing on finding ways to bring Laravel and Domain Driven Design closer together. Because I love Laravel, but its architecture sucks .

So today, we’re going to look at how to implement aggregates using Laravel & Eloquent.

Let’s get started!



What’s an aggregate?

A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it’s useful to treat the order (together with its line items) as a single aggregate.
— Martin Fowler, DDD_Aggregate

Aggregates are not just clusters of data and behavior. The primary reason for the pattern is to protect business invariants for a single aggregate instance in a single transaction.
— Vaughn Vernon



What’s an invariant?

Invariants are generally business rules/enforcements/requirements that you impose to maintain the integrity of an object at any given time.
— Nikesh Shetty, Designing the DDD way — Introduction



Example

Let’s implement an OrderAggregate that embodies the following invariants:

  • customers make oders,
  • an order consists of a collection of items,
  • an order is uniquely identified,
  • an order must have a creation timestamp,
  • an order must have a shipment address,
  • it must be possible to calculate the total of an order, which is the sum of its items,
  • all items should have the same currency,
namespace DomainModel;

use AppModelsAddress;
use AppModelsAmount;
use AppModelsCurrency;
use AppModelsCustomerId;
use AppModelsLineItem;
use AppModelsOrder;

class OrderAggregate
{
    /** @param LineItem[] $lineItems */
    public function __construct(
        private Order $root,
        private CustomerId $customerId,
        private Address $shipmentAddress,
        private Currency $currency,
        private array $lineItems = []
    ) {
        $this->root = $root;
        $this->customerId = $customerId;
        $this->shipmentAddress = $shipmentAddress;
        $this->curreny = $currency;
        $this->lineItems = $lineItems;
    }

    public function getRoot(): Order
    {
        return $this->order;
    }

    public function createdAt(): DateTime
    {
        return $this->root->created_at;
    }

    public function getCustomerId(): CustomerId
    {
        return $this->customerId;
    }

    public function getShipmentAddress(): Address
    {
        return $this->shipmentAddress;
    }

    public function getTotal(): Amount
    {
        $total = 0;

        foreach ($this->getLineItems() as $item) {
            $total += $item->unit_price->getValue() * $item->quantity;
        }

        return new Amount($total, $this->currency);
    }

    /** @return LineItem[] */
    public function getLineItems(): array
    {
        return $this->lineItems;
    }

    /** @throws DomainException If $item currency is not compatible with this order */
    public function addLineItem(LineItem $item): void
    {
        if (! $this->curreny->isEqualTo($item->unit_price->currency)) {
            throw new  DomainException(
                "Unable to add item: invalid currency",
            );
        }

        $this->lineItems[] = $item;
    }
}
Enter fullscreen mode

Exit fullscreen mode

Note: if you need help with value objects and Eloquent I wrote an article on the topic



What can we put in an aggregate?

An aggregate will have one of its component objects be the aggregate root. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole.
— Martin Fowler

Beyond that, they may contain:

  • Entities
  • Collections, Lists, Sets, etc.
  • Value objects
  • Value-typed properties (integers, strings, booleans etc.)

You may think of it as a document holding ALL the data necessary to a given transaction (or use case.)

Tip: Eloquent makes it easy to implement lazy-loading in your aggregates. In the above example, we could restructure the getLineItems method so that it loads when it’s used:

public function getLineItems(): array
{
    return $this->getRoot()->items()->get()->toArray();
}
Enter fullscreen mode

Exit fullscreen mode



Can they have commands?

Yes. And they should.

You are not supposed to do:

$car->getEngine()->start();
Enter fullscreen mode

Exit fullscreen mode

But rather:

$car->start();
Enter fullscreen mode

Exit fullscreen mode

Forcing the exposure of aggregate’s internal structure is bad design



How do I persist/retrieve them?

You’re going to use the Repository Pattern:

namespace AppRepositories;

use AppModelsOrder;
use AppModelsOrderAggregate;

class OrderRepository
{
    public function find(int $id): OrderAggregate
    {
        $model = Order::with('items')->findOrFail($id);

        return new OrderAggregate(
            $model,
            $model->customer_id,
            $model->shipment_address,
            $model->currency,
            $model->items->toArray()
        );
    }

    public function store(OrderAggregate $order): void
    {
        DB::transaction(function () use ($order) {
            $order->getRoot()
                ->fill([
                    'customer_id' => $order->getCustomerId(),
                    'shipment_address' => $order->getShipmentAddress(),
                    'currency' => $order->getCurrency(),
                ])
                ->save();

            foreach ($order->getLineItems() as $item) {
                $item->order()->associate($order->getRoot())->save();
            }
        });
    }
}
Enter fullscreen mode

Exit fullscreen mode



Rules for making your aggregates pretty

From the awesome article series by Vaughn Vernon

Rule #1: Keep them small. It is tempting to cram one giant aggregate with anything every use case present and future might need. But it’s a terrible design. You’re going to run into performances and concurrency issues (when several people are working on the same aggregate at the same time).

It’s better to have several representations of order, depending on the broader context, than one. For instance, an order from a cart display page’s point-of-view is not the same as from a billing system.

If we are going to design small aggregates, what does “small” mean? The extreme would be an aggregate with only its globally unique identity and one additional attribute, which is not what’s being recommended […].
Rather, limit the aggregate to just the root entity and a minimal number of attributes and/or value-typed properties. The correct minimum is the ones necessary, and no more.
Smaller aggregates not only perform and scale better, they are also biased toward transactional success, meaning that conflicts preventing [an update] are rare. This makes a system more usable.
— Vaughn Vernon

Rule #2: Model true invariants in consistency boundaries. It sounds barbaric, but it’s pretty simple; it means that, within a single transaction, there is no way one could break the aggregate consistency (its compliance to business rules.)

In other words, it should be impossible to create a bugged version of an aggregate from calling its methods.

One implication of this rule is that a transaction should only commit a single aggregate, since it’s not possible by design to guarantee the consistency of several aggregates at once.

A properly designed aggregate is one that can be modified in any way required by the business with its invariants completely consistent within a single transaction.
And a properly designed bounded context modifies only one aggregate instance per transaction in all cases. What is more, we cannot correctly reason on aggregate design without applying transactional analysis.
Limiting the modification of one aggregate instance per transaction may sound overly strict. However, it is a rule of thumb and should be the goal in most cases. It addresses the very reason to use aggregates.
— Vaughn Vernon

Rule #3: Don’t Trust Every Use Case. Don’t blindly assemble your aggregates based on what the use case specification dictates. They may contain elements that contradict the existing model or force you into committing several aggregates in a single transaction or worse, to model a giant aggregate that fits in a single transaction.

Apply your judgment here and keep in mind that sometimes, the business goal can be achieved using eventual consistency.

The team should critically examine the use cases and challenge their assumptions, especially when following them as written would lead to unwieldy designs.
The team may have to rewrite the use case (or at least re-imagine it if they face an uncooperative business analyst).
The new use case would specify eventual consistency and the acceptable update delay.
— Vaughn Vernon



Conclusion

Murphy’s law states:

Anything that can possibly go wrong, does.

An aggregate is a tactic to mitigates that problem. Within its well-designed boundaries, nothing can go wrong. You can say goodbye to those pesky ifs laying around in your code, handling those cases that are not supposed to happen but happen anyway.

Don’t allow your model to grow beyond your control. Stop using raw data, POPOs, and unguarded Laravel models whose state is uncertain everywhere in your application. Use aggregates instead and connect your model to the actual business your app is supposed to carry.




Thanks for reading

I hope you enjoyed reading this article! If so, please leave a ❤️ or a and consider subscribing! I write posts on PHP, architecture, and Laravel monthly.

A huge thanks to Vaughn Vernon for his review and his articles on DDD

Leave a Reply

Your email address will not be published. Required fields are marked *