Practical Event Sourcing

11,425 views

Published on

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

Published in: Technology
0 Comments
29 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
11,425
On SlideShare
0
From Embeds
0
Number of Embeds
37
Actions
Shares
0
Downloads
84
Comments
0
Likes
29
Embeds 0
No embeds

No notes for slide

Practical Event Sourcing

  1. Sourcing Event Practical @mathiasverraes
  2. Mathias Verraes Student of Systems Meddler of Models Labourer of Legacy verraes.net mathiasverraes
  3. Elephant in the Room Podcast with @everzet elephantintheroom.io @EitRoom
  4. DDDinPHP.org
  5. The Big Picture
  6. Client Write Model Read Model DTO Commands Events CQRS: http://verraes.net/2013/12/fighting-bottlenecks-with-cqrs/
  7. Write Model EventsEvents Read Model This talk
  8. Event Sourcing
  9. Using on object’s history to reconstitute its State
  10. Express history as a series of Domain Events
  11. Something that has happened in the past that is of interest to the business Domain Event
  12. ! happened in the past !
  13. Express history in the Ubiquitous Language
  14. Relevant to the business. ! First class citizens of the Domain Model
  15. Domain Events
  16. interface DomainEvent { /** * @return IdentifiesAggregate */ public function getAggregateId(); }
  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; } }
  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; } }
  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; } }
  20. Domain Events are immutable
  21. RecordsEvents
  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
  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
  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; } ! }
  25. Protecting Invariants
  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”); }) ! );
  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; } // ... }
  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
  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) {…}
  30. Aggregates record events
  31. Aggregates protect invariants
  32. Possible outcomes ! nothing one or more events exception
  33. Aggregates do not expose state
  34. Reconstituting Aggregates
  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 );
  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); }
  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]; }
  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); } !
  39. Projections
  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() } }
  41. Fat events The good kind of duplication
  42. Individual read models for every unique use case
  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'); } } }
  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
  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
  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
  47. Event Store
  48. Immutable Append-only You can’t change history
  49. interface NaiveEventStore { public function commit(DomainEvents $events); ! /** @return AggregateHistory */ public function getAggregateHistoryFor(IdentifiesAggregate $id); ! /** @return DomainEvents */ public function getAll(); } !
  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;
  51. Performance
  52. The Event Store is an immutable, append-only database: infinite caching
  53. Querying events happens by aggregate id only
  54. Read models are faster than joins
  55. Aggregate snapshots, if need be
  56. Testing
  57. // 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() ]);
  58. verraes.net ! joind.in/10911 ! buttercup-php/protects ! mathiasverraes
  59. verraes.net ! joind.in/10911 ! buttercup-php/protects ! mathiasverraes

×