Skip to content

Instantly share code, notes, and snippets.

@MarkRedeman
Last active August 5, 2016 07:31
Show Gist options
  • Select an option

  • Save MarkRedeman/6ff70d731004135c8534f60a337fa6e7 to your computer and use it in GitHub Desktop.

Select an option

Save MarkRedeman/6ff70d731004135c8534f60a337fa6e7 to your computer and use it in GitHub Desktop.
Event sourcing "ligthening" talk

Event sourcing in practice

@MarkRedeman

  • Event Sourcing (ES)
  • Command Query Responsibility Segregation (CQRS)
  • Hexagonal architecture
  • TestDrivenDevelopment (TDD)
  • Domain Driven Design (DDD)
  • Behavior Driven Development (BDD)

And lots of acronyms and other cool words [fn:1].

It’s popular

At the Dutch PHP Conference there was

  • 1 workshop on the basics of CQRS and Event Sourcing
  • 1 keynote about event sourcing
  • 4 talks that had something to do with Event Sourcing [fn:2]

What is event sourcing

  • Domain Events as first class citizens (e.g. UserWasRegistered, ActivityWasPlanned, MembershipFeeWasPayed)
  • Persist entities by only storing their domain events

Examples of events in our application

  • AuthorWasInvitedByEditor.php
  • AuthorWasRegistered.php
  • EditorWasRegistered.php
  • FormularyChangedName.php
  • FormularyCreated.php
  • IndicationWasArchived.php
  • IndicationWasCreated.php
  • IndicationWasPublished.php
  • IndicationWasRenamed.php
  • IndicationWasRestored.php
  • TherapySchemesWereReordered.php
  • TherapySchemeWasAddedToIndication.php
  • TherapySchemeWasRemovedFromIndication.php
  • TherapySchemeWasValidated.php
  • GroupWasCreated.php
  • GroupWasRenamed.php
  • MedicineWasAddedToGroup.php
  • MedicineWasCreated.php
  • MedicineWasRemovedFromGroup.php
  • PreferredMedicineWasChosen.php
  • PreferredMedicineWasRemoved.php
  • GroupWasAppendedToStep.php
  • GroupWasRemovedFromStep.php
  • MedicinesAndGroupsWereReordered.php
  • StepWasArchived.php
  • StepWasCreated.php
  • StepWasRenamed.php
  • StepWasRestored.php
  • UnclassifiedMedicineWasAppendedToStep.php
  • UnclassifiedMedicineWasRemoved.php
  • StepsWereReordered.php
  • StepWasAddedToTherapyScheme.php
  • StepWasRemovedFromTherapyScheme.php
  • TherapySchemeWasArchived.php
  • TherapySchemeWasCreated.php
  • TherapySchemeWasMadeInvalid.php
  • TherapySchemeWasMadeReady.php
  • TherapySchemeWasRenamed.php
  • TherapySchemeWasRestored.php

Pros & Cons

Pros

  • Tell Don’t Ask
  • Loosly coupled code
  • No loss of data
  • Very testable

Cons

  • More boilerplate
  • More complex?
  • Requires you to think before writing code (advantage!?)

Test Driven Development

  • Write event based tests for aggregates & projections
  • Use in memory repositories when testing aggregates & read models
  • Integration tests for generic repositories

Common workflow

Adding new behaviour

Adding new views / reports

Adding new behaviour

  1. Write the test
use Broadway\EventSourcing\Testing\AggregateRootScenarioTestCase;

class BuildingTest extends AggregateRootScenarioTestCase
{
    /** @test */
    function a_user_can_check_in_to_a_building
    {
        $id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');

        $this->scenario
            ->given([
                new BuildingWasRegistered($id)
            ])
            ->when(function (Building $building) {
                $building->checkInUser(string $username)
            })
            ->then([
                new UserCheckedIntoBuilding($id, $username)
            ]);
    }
}
  1. Implement the test
final class Building extends EventSourcedAggregateRoot
{
    // ...

    public function checkInUser(string $username)
    {
        $this->apply(
            new UserCheckedIntoBuilding($this->id, $username)
        );
    }
}
  1. Improve the test
use Broadway\EventSourcing\Testing\AggregateRootScenarioTestCase;

class BuildingTest extends AggregateRootScenarioTestCase
{
    /** @test */
    function a_user_can_check_in_to_a_building
    {
        $id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');

        $this->scenario
            ->given([
                new BuildingWasRegistered($id)
            ])
            ->when(function (Building $building) {
                $building->checkInUser(string $username)
            })
            ->then([
                new UserCheckedIntoBuilding($id, $username)
            ])
            // A user can't check into a building twice
            ->expectExceptionWhen(
                CantCheckIntoBuilding::class,
                function (Building $building) {
                    $building->checkInUser(string $username);
                }
            );
    }
}
  1. Fix the test
final class Building extends EventSourcedAggregateRoot
{
    // ...
    private $checkedInUsers = [];

    public function checkInUser(string $username)
    {
        // Don't let a user check in more than once
        if ($this->userIsCheckedIn($username)) {
            throw CantCheckIntoBuilding::user($username);
        }

        $this->apply(
            new UserCheckedIntoBuilding($this->id, $username)
        );
    }

    private function userIsCheckedIn(string $username) : bool
    {
        return in_array($username, $this->checkedInUsers, true);
    }

    private function whenUserCheckedIntoBuilding(UserCheckedIntoBuilding $event)
    {
        $this->checkedInUsers[] = $event->username();
    }
}

final class CantCHeckIntoBuilding extends \LogicException
{
    public static function user(string $username)
    {
        return new self(sprintf('User "%s" is not checked in', $username));
    }
}

Adding new views / reports

The customer wants a view of all checked in users of a specific building.

  1. Write a read model containing the username data for a building (make it serializable)
class CheckedInUsersTest extends TestCase
{
    /** @test */
    function it_keeps_track_of_the_users_checked_into_a_building()
    {
        $id = new BuildingId('943ed5e8-7058-4b2e-90cd-8b131152faa3');
        $readModel = CheckedInUsers::forBuilding($id);

        $this->assertInstanceOf(CheckedInUsers::class, $readModel);
        $this->assertEquals($id, $readModel->buildingId());
    }

    /** @test */
    function a_user_can_be_added_to_the_read_model()
    {
        $id = new BuildingId('943ed5e8-7058-4b2e-90cd-8b131152faa3');
        $readModel = CheckedInUsers::forBuilding($id);
        $readModel->addUsername("Mark");

        $this->assertEquals(["Mark"], $readModel->usernames());
    }

    // .. etc.
}
final class CheckedInUsers
{
    /** @var string[] */
    private $usersnames;
    /** @var string */
    private $buildingId;

    public static function forBuilding(BuildingId $id) : CheckedInUsers
    {
        $model = new self;
        $model->buildingId = $id;
        return $model;
    }

    public function addUsername(string $username)
    {
        $this->usernames[] = $username;
    }

    public function usernames() : array
    {
        return $this->usernames;
    }

    public function buildingId() : BuildingId
    {
        return new BuildingId($this->buildingId);
    }

    // some stuff to make it serializable
}
  1. Write a projection that populates the read model
class CheckedInUsersProjectorTest extends ProjectorScenarioTestCase
{
    protected function createProjector(InMemoryRepository $repository)
    {
        return new CheckedInUsersProjector($repository);
    }

    /** @test */
    function it_starts_keeping_track_of_a_buildings_checked_in_users()
    {
        $id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');

        $this->scenario->when(
            new BuildingWasRegistered($id)
        )->then(function (InMemoryRepository $repo) use ($id) {
            // Check that our in memory repository contains the building
            $this->assertEquals(
                CheckedInUsers::forBuilding($id),
                $repo->find($id)
            );
        }]);
    }

    /** @test */
    function it_keeps_track_of_a_buildings_checked_in_users()
    {
        $id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');

        $this->scenario->when(
            new BuildingWasRegistered($id),
            new UserCheckedIntoBuilding($id, "Mark"),
            new UserCheckedIntoBuilding($id, "Piet"),
            new UserCheckedIntoBuilding($id, "Klaas")
        )->then(function (InMemoryRepository $repo) use ($id) {
                $readModel = $repo->find($id);

                $this->assertEquals(
                    ["Mark", "Piet", "Klaas"],
                    $readModel->usernames()
                );
            }
        );
    }
}
  1. Persist the read model

Future stuff

  • Improve testing of read models (lots of duplicated code)

Footnotes

Open source event sourced application: Professor Francken

[fn:1] See Matthias Noback’s talk “All the cool kids…”

[fn:2] Talks:

  • Integrating Bounded Contexts (Carlos Buenosvinos)
  • CQRS and Domain Events for integration (Giorgio Sironi)
  • Event Sourcing: the good, the bad and the complicated (Marco Pivetta)
  • How I Built A Video Game using Event Sourcing (Shawn McCool)

Other interesting talks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment