Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wrong. All that paired with a developer friendly API and kick-ass documentation. O, and you'll also be able to create a public status page under a minute. Start monitoring using our free trial now.

Implementing event sourcing: aggregates

Original – by Freek Van der Herten – 4 minute read

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.

Stay up to date with all things Laravel, PHP, and JavaScript.

You can follow me on these platforms:

On all these platforms, regularly share programming tips, and what I myself have learned in ongoing projects.

Every month I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

What are your thoughts on "Implementing event sourcing: aggregates"?

Comments powered by Laravel Comments
Want to join the conversation? Log in or create an account to post a comment.