Implementing event sourcing: aggregates
Recently we've released v2 of laravel-event-projector. The package is probably the easiest way to get started with event sourcing in Laravel.
One of the prominent new features is support for aggregates. While creating v2, I found it surprising that such a powerful concept could be implemented in so little code. In this short blog post, I'd like to explain how aggregates are coded up.
If you don't know anything about event sourcing or don't know what aggregates are, head over to the docs of the package. It contains an entire introduction targeted at newcomers. The rest of this post assumes that you know what aggregates are and that you know how to work with them.
Reconstituting an aggregate from previous events
Before you can work with an aggregate, it must be reconstituted from all previous events for a given uuid. As a package user, you can do this with MyAggreggate::retrieve($uuid)
. Here is the implementation of that function.
public static function retrieve(string $uuid): AggregateRoot
{
$aggregateRoot = (new static());
$aggregateRoot->aggregateUuid = $uuid;
return $aggregateRoot->reconstituteFromEvents();
}
The $uuid
will be kept as an instance variable aggregateUuid
. The actual reconstituting happens in reconstituteFromEvents
. Let's take a look at that code.
private function reconstituteFromEvents(): AggregateRoot
{
StoredEvent::uuid($this->aggregateUuid)->each(function (StoredEvent $storedEvent) {
$this->apply($storedEvent->event);
});
return $this;
}
That ::uuid
is just a scope that defined on the StoredEventModel
. That each
call is being called on a Builder
instance, not a Collection
. Under the hood that each
call will fetch all events in a chunked way.
Let's take a look at that apply
method. It will convert the class name of an event to a method name. App\Event\MoneyAdded
will be converted to applyMoneyAdded
. If such a method with that name exists on the aggregate, it will get called with the event as the argument.
private function apply(ShouldBeStored $event): void
{
$classBaseName = class_basename($event);
$camelCasedBaseName = ucfirst(Str::camel($classBaseName));
$applyingMethodName = "apply{$camelCasedBaseName}";
if (method_exists($this, $applyingMethodName)) {
$this->$applyingMethodName($event);
}
}
And that's basically all the code needed to reconstitute an aggregate from past events.
Recording new events
When an aggregate is persisted all new events it recorded should be stored. This is very easy to accomplish. An aggregate offers a recordThat
function to record new events.
public function recordThat(ShouldBeStored $event): AggregateRoot
{
$this->recordedEvents[] = $event;
$this->apply($domainEvent);
return $this;
}
That function keeps the events in array on the instance. It also immediately applies it onto the aggregate (using that apply
method we already reviewed above).
When calling persist
on the aggregate, all the events will be passed to the storeMany
method of the StoredEvent
model. The $recordedEvents
array will be emptied so we don't record the same events should persist
be called again on this instance of the aggregate.
public function persist(): AggregateRoot
{
StoredEvent::storeMany(
$this->getAndClearRecoredEvents(),
$this->aggregateUuid,
);
return $this;
}
private function getAndClearRecoredEvents(): array
{
$recordedEvents = $this->recordedEvents;
$this->recordedEvents = [];
return $recordedEvents;
}
Because the actual storing of the events in storeMany
isn't a responsibility of the aggregate, we're not going to go over it in detail. In short, storeMany
will store the events, pass them to all registered projectors and will dispatch a job to pass them to all queued projectors and reactors.
And with that, we've implemented aggregates.
What are your thoughts on "Implementing event sourcing: aggregates"?