Projections and Read Models

Projections and read models are a big part of event sourcing. They are a way to communicating state with the outside world. They’re also very project-specific.

In EventSauce projections are nothing more than the implementation of the MessageConsumer interface.

<?php

namespace EventSauce\EventSourcing;

interface MessageConsumer
{
    public function handle(Message $message);
}

The MessageConsumer accepts a Message via the handle method. It can then retrieve the event from the Message to read information about something important that happened in the business.

Why read models are important/useful

Read models allow you to separate presentational state from the process you’re modeling. In general this has two effects. Your processing side is leaner because it doesn’t have to deal with any presentation data or associated presentation logic. On the other your read models are free from any constraints your domain model has and can be very optimized.

Because projections and read models are fed by a stream of events it’s also a lot easier to create multiple read models. These read models can even be specific to one use-case and don’t have to share restrictions.

Read model example: friendship requests

As an example we’re going to create a couple of read models for the following case: becoming friends on social media. In this case we’ll have the following events defined:

  • FriendshipRequestWasSent
  • FriendshipRequestWasCancelled
  • FriendshipRequestWasAccepted
  • FriendshipRequestWasDenied

All these events describe something that happened in the process of becoming friends on social media.

In our UI we might have two views. One for outgoing invitations and one for incoming. We can map these UI’s to two read models:

  • OutgoingInvitations
  • IncomingInvitations

These two read models could very well be placed in a single read model, but for the purpose of our demonstration we’ll separate them.

For each of the read models we can add a projection:

<?php

use EventSauce\EventSourcing\MessageConsumer;
use EventSauce\EventSourcing\Message;

class PendingInvitationProjection implements MessageConsumer
{
    public function __construct(PendingInvitations $invitations)
    {
        $this->invitations = $invitations;
    }

    public function handle(Message $message): void
    {
        $event = $message->payload();

        if ($event instanceof FriendshipRequestWasSent) {
            $this->invitations->add(new FriendshipRequest(
                $event->requestId(),
                $event->fromUser(),
                $event->toUser(),
                RequestStatus::pending()
            ));
        } elseif ($event instanceof FriendshipRequestWasCancelled) {
            $this->invitations->updateStatus($event->requestId(), RequestStatus::cancelled());
        } elseif ($event instanceof FriendshipRequestWasAccepted) {
            $this->invitations->updateStatus($event->requestId(), RequestStatus::accepted());
        } elseif ($event instanceof FriendshipRequestWasDenied) {
            // Just remove the request to prevent sad feelings.
            $this->invitations->removeRequest($event->requestId());
        }
    }
}

Event Consumers

In the previous example we had to manually check the type of the event to react appropriately. EventConsumer makes it easier to handle different event types. The default strategy takes the class name of the event and transforms it to a method name. For example, FriendshipRequestWasSent becomes handleFriendshipRequestWasSent.

<?php

use EventSauce\EventSourcing\EventConsumption\EventConsumer;

class PendingInvitationProjection extends EventConsumer
{
    public function handleFriendshipRequestWasSent(FriendshipRequestWasSent $event, Message $message): void
    {
        $this->invitations->add(new FriendshipRequest(
            $event->requestId(),
            $event->fromUser(),
            $event->toUser(),
            RequestStatus::pending()
        ));
    }
}

A different strategy uses reflection to determine which handler methods accept the event type. This allows you to handle multiple events in a single method using a union type and multiple methods for a single event.

<?php

use EventSauce\EventSourcing\MessageConsumer;
use EventSauce\EventSourcing\EventConsumption\EventConsumer;
use EventSauce\EventSourcing\EventConsumption\HandleMethodInflector;
use EventSauce\EventSourcing\EventConsumption\InflectHandlerMethodsFromType;

class PendingInvitationProjection extends EventConsumer
{
    protected function handleMethodInflector(): HandleMethodInflector
    {
        return new InflectHandlerMethodsFromType();
    }

    // Note, the handler methods must be public.
    public function addInvitation(FriendshipRequestWasSent $event, Message $message): void
    {
        $this->invitations->add(new FriendshipRequest(
            $event->requestId(),
            $event->fromUser(),
            $event->toUser(),
            RequestStatus::pending()
        ));
    }

    public function increaseResponseCount(FriendshipRequestWasAccepted|FriendshipRequestWasDenied $event, Message $message): void
    {
        $this->responses++;
    }
}
Frank de Jonge

EventSauce is a project by Frank de Jonge.