Download pdf - Practical Event Sourcing

Transcript
Page 1: Practical Event Sourcing

SourcingEventPractical

@mathiasverraes

Page 2: Practical Event Sourcing

Mathias VerraesStudent of Systems Meddler of Models Labourer of Legacy

verraes.net mathiasverraes

Page 3: Practical Event Sourcing

Elephant in the Room Podcast with @everzet

elephantintheroom.io @EitRoom

Page 4: Practical Event Sourcing

DDDinPHP.org

Page 5: Practical Event Sourcing

The Big Picture

Page 6: Practical Event Sourcing

Client

Write Model

Read Model

DTOCommands

Even

ts

CQRS: http://verraes.net/2013/12/fighting-bottlenecks-with-cqrs/

Page 7: Practical Event Sourcing

Write Model

Even

tsEv

ents

Read Model

This talk

Page 8: Practical Event Sourcing

Event Sourcing

Page 9: Practical Event Sourcing

Using on object’s

history to reconstitute its

State

Page 10: Practical Event Sourcing

Express

history as a series of

Domain Events

Page 11: Practical Event Sourcing

Something that has happened in the past

that is of interest to the business

Domain Event

Page 12: Practical Event Sourcing

!

happened in the past !

Page 13: Practical Event Sourcing

Express

history in the

Ubiquitous Language

Page 14: Practical Event Sourcing

Relevant to the business.

!

First class citizens of the Domain Model

Page 15: Practical Event Sourcing

Domain Events

Page 16: Practical Event Sourcing

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

Page 17: Practical Event Sourcing

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; } }

Page 18: Practical Event Sourcing

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; } }

Page 19: Practical Event Sourcing

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; } }

Page 20: Practical Event Sourcing

Domain Events are

immutable

Page 21: Practical Event Sourcing

RecordsEvents

Page 22: Practical Event Sourcing

$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

Page 23: Practical Event Sourcing

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

Page 24: Practical Event Sourcing

// 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; } !}

Page 25: Practical Event Sourcing

Protecting Invariants

Page 26: Practical Event Sourcing

$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”); }) !);

Page 27: Practical Event Sourcing

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; } // ... }

Page 28: Practical Event Sourcing

$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

234

Page 29: Practical Event Sourcing

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) {…}

Page 30: Practical Event Sourcing

Aggregates record events

Page 31: Practical Event Sourcing

Aggregates protect invariants

Page 32: Practical Event Sourcing

Possible outcomes !

nothing one or more events

exception

Page 33: Practical Event Sourcing

Aggregates do not expose state

Page 34: Practical Event Sourcing

Reconstituting Aggregates

Page 35: Practical Event Sourcing

!$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 );

Page 36: Practical Event Sourcing

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); }

Page 37: Practical Event Sourcing

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]; }

Page 38: Practical Event Sourcing

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); } !

Page 39: Practical Event Sourcing

Projections

Page 40: Practical Event Sourcing

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() } }

Page 41: Practical Event Sourcing

Fat events The good kind of duplication

Page 42: Practical Event Sourcing

Individual read models for every unique

use case

Page 43: Practical Event Sourcing

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'); } } }

Page 44: Practical Event Sourcing

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

Page 45: Practical Event Sourcing

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

Page 46: Practical Event Sourcing

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

Page 47: Practical Event Sourcing

Event Store

Page 48: Practical Event Sourcing

Immutable Append-only

You can’t change history

Page 49: Practical Event Sourcing

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

Page 50: Practical Event Sourcing

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;

Page 51: Practical Event Sourcing
Page 52: Practical Event Sourcing

Performance

Page 53: Practical Event Sourcing

The Event Store is an immutable, append-only

database: infinite caching

Page 54: Practical Event Sourcing

Querying events happens by aggregate id only

Page 55: Practical Event Sourcing

Read models are faster than joins

Page 56: Practical Event Sourcing

Aggregate snapshots, if need be

Page 57: Practical Event Sourcing

Testing

Page 58: Practical Event Sourcing

// 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() ]);

Page 59: Practical Event Sourcing

verraes.net !

joind.in/10911 !

buttercup-php/protects !

mathiasverraes

Page 60: Practical Event Sourcing

verraes.net !

joind.in/10911 !

buttercup-php/protects !

mathiasverraes