$30 off During Our Annual Pro Sale. View Details »

Practical Event Sourcing

Practical Event Sourcing

Traditionally, we create structural models for our applications, and store the state of these models in our databases.

But there are alternatives: Event Sourcing is the idea that you can store all the domain events that affect an entity, and replay these events to restore the object's state. This may sound counterintuitive, because of all the years we've spent building relational, denormalized database schemas. But it is in fact quite simple, elegant, and powerful.

In the past year, I've had the pleasure of building and shipping two event sourced systems. In this session, I will show practical code, to give you a feel of how you can build event sourced models using PHP.

Mathias Verraes is a recovering music composer turned programmer, consultant, blogger, speaker, and podcaster. He advises companies on how to build enterprise web applications for complex business domains . For some weird reason, he enjoys working on large legacy projects: the kind where there’s half a million lines of spaghetti code, and nobody knows how to get the codebase under control. He’s the founder of the Domain-Driven Design Belgium community. When he’s not working, he’s at home in Kortrijk, Belgium, helping his two sons build crazy Lego train tracks.

http://verraes.net

Mathias Verraes

March 18, 2014
Tweet

More Decks by Mathias Verraes

Other Decks in Technology

Transcript

  1. Sourcing
    Event
    Practical
    @mathiasverraes

    View Slide

  2. Mathias Verraes
    Student of Systems
    Meddler of Models
    Labourer of Legacy
    verraes.net
    mathiasverraes

    View Slide

  3. Elephant in the Room
    Podcast with @everzet
    elephantintheroom.io
    @EitRoom

    View Slide

  4. DDDinPHP.org

    View Slide

  5. The Big Picture

    View Slide

  6. Client
    Write
    Model
    Read
    Model
    DTO
    Commands
    Events
    CQRS: http://verraes.net/2013/12/fighting-bottlenecks-with-cqrs/

    View Slide

  7. Write
    Model
    Events
    Events
    Read
    Model
    This talk

    View Slide

  8. Event Sourcing

    View Slide

  9. Using on object’s
    history
    to reconstitute its
    State

    View Slide

  10. Express
    history
    as a series of
    Domain Events

    View Slide

  11. Something that has
    happened in the past
    that is of
    interest to the business
    Domain Event

    View Slide

  12. !
    happened in the past
    !

    View Slide

  13. Express
    history
    in the
    Ubiquitous Language

    View Slide

  14. Relevant
    to the business.
    !
    First class citizens of the
    Domain Model

    View Slide

  15. Domain Events

    View Slide

  16. interface DomainEvent
    {
    /**
    * @return IdentifiesAggregate
    */
    public function getAggregateId();
    }

    View Slide

  17. final class ProductWasAddedToBasket implements DomainEvent
    {
    private $basketId, $productId, $productName;
    !
    public function __construct(
    BasketId $basketId, ProductId $productId, $productName
    ) {
    $this->basketId = $basketId;
    $this->productName = $productName;
    $this->productId = $productId;
    }
    !
    public function getAggregateId()
    {
    return $this->basketId;
    }
    !
    public function getProductId()
    {
    return $this->productId;
    }
    !
    public function getProductName()
    {
    return $this->productName;
    }
    }

    View Slide

  18. final class ProductWasRemovedFromBasket implements DomainEvent
    {
    private $basketId;
    private $productId;
    !
    public function __construct(BasketId $basketId, ProductId $productId)
    {
    $this->basketId = $basketId;
    $this->productId = $productId;
    }
    !
    public function getAggregateId()
    {
    return $this->basketId;
    }
    !
    public function getProductId()
    {
    return $this->productId;
    }
    }

    View Slide

  19. final class BasketWasPickedUp implements DomainEvent
    {
    private $basketId;
    !
    public function __construct(BasketId $basketId)
    // You may want to add a date, user, …
    {
    $this->basketId = $basketId;
    }
    !
    public function getAggregateId()
    {
    return $this->basketId;
    }
    }

    View Slide

  20. Domain Events
    are
    immutable

    View Slide

  21. RecordsEvents

    View Slide

  22. $basket = Basket::pickUp(BasketId::generate());
    $basket->addProduct(new ProductId('AV001'), “The Last Airbender");
    $basket->removeProduct(new ProductId('AV001'));
    !
    !
    $events = $basket->getRecordedEvents();
    !
    it("should have recorded 3 events",
    3 == count($events));
    !
    it("should have a BasketWasPickedUp event",
    $events[0] instanceof BasketWasPickedUp);
    !
    it("should have a ProductWasAddedToBasket event",
    $events[1] instanceof ProductWasAddedToBasket);
    !
    it("should have a ProductWasRemovedFromBasket event",
    $events[2] instanceof ProductWasRemovedFromBasket);
    !
    !
    // Output:
    ✔ It should have recorded 3 events
    ✔ It should have a BasketWasPickedUp event
    ✔ It should have a ProductWasAddedToBasket event
    ✔ It should have a ProductWasRemovedFromBasket event
    TestFrameworkInATweet https://gist.github.com/mathiasverraes/9046427

    View Slide

  23. final class Basket implements RecordsEvents
    {
    public static function pickUp(BasketId $basketId)
    {
    $basket = new Basket($basketId);
    $basket->recordThat(
    new BasketWasPickedUp($basketId)
    );
    return $basket;
    }
    !
    public function addProduct(ProductId $productId, $name)
    {
    $this->recordThat(
    new ProductWasAddedToBasket($this->basketId, $productId, $name)
    );
    }
    !
    public function removeProduct(ProductId $productId)
    {
    $this->recordThat(
    new ProductWasRemovedFromBasket($this->basketId, $productId)
    );
    }
    !
    // continued on next slide

    View Slide

  24. // continued: final class Basket implements RecordsEvents
    !
    private $basketId;
    !
    private $latestRecordedEvents = [];
    !
    private function __construct(BasketId $basketId)
    {
    $this->basketId = $basketId;
    }
    !
    public function getRecordedEvents()
    {
    return new DomainEvents($this->latestRecordedEvents);
    }
    !
    public function clearRecordedEvents()
    {
    $this->latestRecordedEvents = [];
    }
    !
    private function recordThat(DomainEvent $domainEvent)
    {
    $this->latestRecordedEvents[] = $domainEvent;
    }
    !
    }

    View Slide

  25. Protecting Invariants

    View Slide

  26. $basket = Basket::pickUp(BasketId::generate());
    !
    $basket->addProduct(new ProductId('AV1'), “The Last Airbender");
    $basket->addProduct(new ProductId('AV2'), "The Legend of Korra");
    $basket->addProduct(new ProductId('AV3'), “The Making Of Avatar”);
    !
    it("should disallow adding a fourth product",
    throws(‘BasketLimitReached’, function () use($basket) {
    $basket->addProduct(new ProductId('AV4'), “The Last Airbender Movie”);
    })
    !
    );

    View Slide

  27. final class Basket implements RecordsEvents
    {
    private $productCount = 0;
    !
    public function addProduct(ProductId $productId, $name)
    {
    $this->guardProductLimit();
    $this->recordThat(
    new ProductWasAddedToBasket($this->basketId, $productId, $name)
    );
    ++$this->productCount;
    }
    !
    private function guardProductLimit()
    {
    if ($this->productCount >= 3) {
    throw new BasketLimitReached;
    }
    }
    !
    public function removeProduct(ProductId $productId)
    {
    $this->recordThat(
    new ProductWasRemovedFromBasket($this->basketId, $productId)
    );
    --$this->productCount;
    }
    // ...
    }

    View Slide

  28. $basket = Basket::pickUp(BasketId::generate());
    !
    $productId = new ProductId(‘AV1');
    !
    $basket->addProduct($productId, “The Last Airbender");
    $basket->removeProduct($productId);
    $basket->removeProduct($productId);
    !
    it(“shouldn't record an event when removing a Product
    that is no longer in the Basket”,
    !
    count($basket->getRecordedEvents()) == 3
    !
    );
    1
    2
    3
    4

    View Slide

  29. final class Basket implements RecordsEvents
    {
    private $productCountById = [];
    !
    public function addProduct(ProductId $productId, $name)
    {
    $this->guardProductLimit();
    $this->recordThat(new ProductWasAddedToBasket(…));
    !
    if(!$this->productIsInBasket($productId)) {
    $this->productCountById[$productId] = 0;
    }
    !
    ++$this->productCountById[$productId];
    }
    !
    public function removeProduct(ProductId $productId)
    {
    if(! $this->productIsInBasket($productId)) {
    return;
    }
    !
    $this->recordThat(new ProductWasRemovedFromBasket(…);
    !
    --$this->productCountById;
    }
    private function productIsInBasket(ProductId $productId) {…}

    View Slide

  30. Aggregates
    record events

    View Slide

  31. Aggregates
    protect invariants

    View Slide

  32. Possible outcomes
    !
    nothing
    one or more events
    exception

    View Slide

  33. Aggregates do not
    expose state

    View Slide

  34. Reconstituting
    Aggregates

    View Slide

  35. !
    $basket = Basket::pickUp($basketId);
    $basket->addProduct($productId, “The Last Airbender");
    !
    $events = $basket->getRecordedEvents();
    !
    // persist events in an event store, retrieve at a later time
    !
    $reconstitutedBasket = Basket::reconstituteFrom(
    new AggregateHistory($basketId, $retrievedEvents)
    );
    !
    it("should be the same after reconstitution",
    $basket == $reconstitutedBasket
    );

    View Slide

  36. final class Basket implements RecordsEvents, IsEventSourced
    {
    public function addProduct(ProductId $productId, $name)
    {
    $this->guardProductLimit();
    $this->recordThat(new ProductWasAddedToBasket(…));
    !
    // No state is changed!
    }
    !
    public function removeProduct(ProductId $productId)
    {
    if(! $this->productIsInBasket($productId)) {
    return;
    }
    !
    $this->recordThat(new ProductWasRemovedFromBasket(…));
    !
    // No state is changed!
    }
    !
    private function recordThat(DomainEvent $domainEvent)
    {
    $this->latestRecordedEvents[] = $domainEvent;
    !
    $this->apply($domainEvent);
    }

    View Slide

  37. private function applyProductWasAddedToBasket(
    ProductWasAddedToBasket $event)
    {
    !
    $productId = $event->getProductId();
    !
    if(!$this->productIsInBasket($productId)) {
    $this->products[$productId] = 0;
    }
    !
    ++$this->productCountById[$productId];
    !
    }
    !
    private function applyProductWasRemovedFromBasket(
    ProductWasRemovedFromBasket $event)
    {
    $productId = $event->getProductId();
    --$this->productCountById[$productId];
    }

    View Slide

  38. public static function reconstituteFrom(
    AggregateHistory $aggregateHistory)
    {
    $basketId = $aggregateHistory->getAggregateId();
    $basket = new Basket($basketId);
    !
    foreach($aggregateHistory as $event) {
    $basket->apply($event);
    }
    return $basket;
    }
    !
    private function apply(DomainEvent $event)
    {
    $method = 'apply' . get_class($event);
    $this->$method($event);
    }
    !

    View Slide

  39. Projections

    View Slide

  40. final class BasketProjector
    {
    public function projectProductWasAddedToBasket(
    ProductWasAddedToBasket $event)
    {
    INSERT INTO baskets_readmodel
    SET
    `basketId` = $event->getBasketId(),
    `productId` = $event->getProductId(),
    `name` = $event->getName()
    }
    public function projectProductWasRemovedFromBasket(
    ProductWasRemovedFromBasket $event)
    {
    DELETE FROM baskets_readmodel
    WHERE
    `basketId` = $event->getBasketId()
    AND `productId` = $event->getProductId()
    }
    }

    View Slide

  41. Fat events
    The good kind of duplication

    View Slide

  42. Individual read models for
    every unique
    use case

    View Slide

  43. final class BlueProductsSoldProjection
    {
    public function projectProductWasIntroducedInCatalog(
    ProductWasIntroducedInCatalog $event)
    {
    if($event->getColor() == 'blue') {
    $this->redis->sAdd('blueProducts', $event->getProductId());
    }
    }
    !
    public function projectProductWasAddedToBasket(
    ProductWasAddedToBasket $event)
    {
    if($this->redis->sIsMember($event->getProductId())) {
    $this->redis->incr('blueProductsSold');
    }
    }
    !
    public function projectProductWasRemovedFromBasket(
    ProductWasRemovedFromBasket $event)
    {
    if($this->redis->sIsMember($event->getProductId())) {
    $this->redis->decr('blueProductsSold');
    }
    }
    }

    View Slide

  44. LessonWasScheduled
    { SchoolId, GroupId, TeacherId, Subject, WeekDay, Timeslot }
    !
    =>
    !
    GroupScheduleProjector
    Group 1A Monday Tuesday Wednesday Thursday Friday
    09:00
    Math
    Ada
    German
    Friedrich
    Math
    Ada
    Chemistry
    Niels
    Economy
    Nicholas
    10:00
    French
    Albert
    Math
    Ada
    Physics
    Isaac
    PHP
    Rasmus
    History
    Julian
    11:00
    Sports
    Felix
    PHP
    Rasmus
    PHP
    Rasmus
    German
    Friedrich
    Math
    Ada

    View Slide

  45. LessonWasScheduled
    { SchoolId, GroupId, TeacherId, Subject, WeekDay, Timeslot }
    !
    =>
    !
    TeacherScheduleProjector
    Ada!
    Math
    Monday Tuesday Wednesday Thursday Friday
    09:00
    Group 1A
    School 5
    Group 1A
    School 5
    Group 6C
    School 9
    Group 5B
    School 9
    10:00
    Group 1B
    School 5
    Group 1A
    School 5
    Group 6C
    School 9
    Group 5B
    School 9
    11:00
    Group 2A
    School 5
    Group 5B
    School 9
    Group 1A
    School 5

    View Slide

  46. PupilWasEnlistedInGroup
    { PupilId, SchoolId, GroupId }
    LessonWasScheduled
    { SchoolId, GroupId, TeacherId, Subject, WeekDay, Timeslot }
    !
    =>
    !
    TeacherPermissionsProjector
    Ada Pupil 1
    Ada Pupil 3
    Friedrich Pupil 1
    Friedrich Pupil 7
    Ada Pupil 8
    Julian Pupil 3

    View Slide

  47. Event Store

    View Slide

  48. Immutable
    Append-only
    You can’t change history

    View Slide

  49. interface NaiveEventStore
    {
    public function commit(DomainEvents $events);
    !
    /** @return AggregateHistory */
    public function getAggregateHistoryFor(IdentifiesAggregate $id);
    !
    /** @return DomainEvents */
    public function getAll();
    }
    !

    View Slide

  50. CREATE TABLE `buttercup_eventstore` (
    `streamId` varbinary(16) NOT NULL,
    `streamVersion` bigint(20) unsigned NOT NULL,
    `streamContract` varchar(255) NOT NULL,
    `eventDataContract` varchar(255) NOT NULL,
    `eventData` text NOT NULL,
    `eventMetadataContract` varchar(255) NOT NULL,
    `eventMetadata` text NOT NULL,
    `utcStoredTime` datetime NOT NULL,
    `correlationId` varbinary(16) NOT NULL,
    `causationId` varbinary(16) NOT NULL,
    `causationEventOrdinal` bigint(20) unsigned,
    PRIMARY KEY (`streamId`,`streamVersion`,`streamContract`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    View Slide

  51. View Slide

  52. Performance

    View Slide

  53. The Event Store is an
    immutable, append-only
    database:
    infinite caching

    View Slide

  54. Querying events happens by
    aggregate id only

    View Slide

  55. Read models are
    faster than joins

    View Slide

  56. Aggregate snapshots,
    if need be

    View Slide

  57. Testing

    View Slide

  58. // it should disallow evaluating pupils without planning them first
    !
    $scenario->given([
    new EvaluationWasPlanned(…)
    ]);
    !
    $scenario->when(
    new EvaluatePupil(…)
    );
    !
    $scenario->then([
    $scenario->throws(new CantEvaluateUnplannedPupil(…))
    ]);
    !
    ——————————————————————————————————————————————————————————————————————————-
    !
    $scenario->given([
    new EvaluationWasPlanned(…),
    new PupilWasPlannedForEvaluation(…)
    ]);
    !
    $scenario->when(
    new EvaluatePupil(…)
    );
    !
    $scenario->then([
    new PupilWasEvaluated()
    ]);

    View Slide

  59. verraes.net
    !
    joind.in/10911
    !
    buttercup-php/protects
    !
    mathiasverraes

    View Slide

  60. verraes.net
    !
    joind.in/10911
    !
    buttercup-php/protects
    !
    mathiasverraes

    View Slide