This content originally appeared on DEV Community and was authored by Benjamin Delespierre
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 IS 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
So, an aggregate is a bag of domain objects that represents something meaningful. Let's consider Fowler's order example:
namespace Domain\Model;
use App\Models\Order;
use App\Models\LineItem;
class OrderAggregate
{
private Order $root;
/** @var LineItem* */
private array $lineItems;
public function __construct(Order $root, array $lineItems = [])
{
$this->root = $root;
$this->lineItems = $lineItems;
}
public function getRoot(): Order
{
return $this->order;
}
/** @return LineItem* */
public function getLineItems(): array
{
return $this->lineItems;
}
}
In this aggregate, the domain objects are our Eloquent models Order
and LineItem
. It also embodies the following business rule: "An order consists of an identifier, a creation date, and zero or more line items." (That business rule is sometimes referred to as an invariant.)
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();
}
Can they have commands?
Yes. And they should.
You are not supposed to do:
$car->getEngine()->start();
But rather:
$car->start();
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 App\Repositories;
class OrderRepository
{
public function find(int $id): OrderAggregate
{
$orderEntity = Order::with('items')->findOrFail($id);
return new OrderAggregate(
$orderEntity,
$orderEntity->items->toArray()
);
}
public function store(OrderAggregate $order): void
{
DB::transaction(function () use ($order) {
$order->getRoot()->save();
foreach ($order->getLineItems() as $item) {
$item->order()->associate($order->getRoot())->save();
}
});
}
}
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.
Properly designed aggregates guarantees that, within its boundaries, nothing can go wrong (well, if you write them according to the rules above, of course.) You can say goodbye to those 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 models whose state is uncertain everywhere in your Laravel 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 on a monthly basis.
A huge thanks to Vaughn Vernon for his articles on DDD ?
This content originally appeared on DEV Community and was authored by Benjamin Delespierre
Benjamin Delespierre | Sciencx (2021-09-20T02:07:18+00:00) Domain Driven Design Aggregates in Laravel. Retrieved from https://www.scien.cx/2021/09/20/domain-driven-design-aggregates-in-laravel/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.