practical event sourcing

60
Sourcing Event Practical @mathiasverraes

Upload: mathiasverraes

Post on 06-May-2015

5.281 views

Category:

Technology


1 download

DESCRIPTION

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

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