DEV Community

Freek Van der Herten
Freek Van der Herten

Posted on • Originally published at freek.dev on

★ 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.

Top comments (0)