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

More Related Content

PDF
Session 2 - NGSI-LD primer & Smart Data Models | Train the Trainers Program
PDF
Models and Service Layers, Hemoglobin and Hobgoblins
PDF
Styled components presentation
PPT
Functions in C++
PDF
Cross-domain requests with CORS
PPTX
Docker Ecosystem on Azure
PDF
Java programming lab manual
PPT
Java: Inheritance
Session 2 - NGSI-LD primer & Smart Data Models | Train the Trainers Program
Models and Service Layers, Hemoglobin and Hobgoblins
Styled components presentation
Functions in C++
Cross-domain requests with CORS
Docker Ecosystem on Azure
Java programming lab manual
Java: Inheritance

What's hot (20)

PDF
Socket Programming In Python
PPT
Intro to CloudStack API
PPTX
Polymorphism in Python
PPT
Java collection
PPTX
ReactPHP + Symfony
PPT
Strings In OOP(Object oriented programming)
PDF
FIWARE Training: JSON-LD and NGSI-LD
PPTX
Array of objects.pptx
PPTX
Hydra: A Vocabulary for Hypermedia-Driven Web APIs
PPTX
Presentation on overloading
PDF
What is Python JSON | Edureka
DOC
El requerimiento
PDF
PDF
Your code sucks, let's fix it
DOCX
Python lab manual all the experiments are available
PPTX
Inheritance In Java
PPTX
L14 exception handling
PPTX
JavaScript Promises
PPTX
Reusable, composable, battle-tested Terraform modules
PPT
Operators in C++
Socket Programming In Python
Intro to CloudStack API
Polymorphism in Python
Java collection
ReactPHP + Symfony
Strings In OOP(Object oriented programming)
FIWARE Training: JSON-LD and NGSI-LD
Array of objects.pptx
Hydra: A Vocabulary for Hypermedia-Driven Web APIs
Presentation on overloading
What is Python JSON | Edureka
El requerimiento
Your code sucks, let's fix it
Python lab manual all the experiments are available
Inheritance In Java
L14 exception handling
JavaScript Promises
Reusable, composable, battle-tested Terraform modules
Operators in C++
Ad

Similar to Practical Event Sourcing (20)

PPTX
Event sourcing with the GetEventStore
ODP
Rich domain model with symfony 2.5 and doctrine 2.5
PDF
Event sourcing w PHP (by Piotr Kacała)
PDF
My first year with event sourcing-nijmegen
PPTX
Event Sourcing with php
PDF
From ActiveRecord to EventSourcing
PDF
My first year with event sourcing-symfonycon
PPT
Domain driven design
PDF
Domain-driven Design in PHP and Symfony - Drupal Camp Wroclaw!
PDF
Baby steps to Domain-Driven Design
PDF
Developing microservices with aggregates (melbourne)
PDF
Developing microservices with aggregates (devnexus2017)
PDF
Developing microservices with aggregates (SpringOne platform, #s1p)
PDF
Unbreakable Domain Models - DPC13
PDF
fundamentalsofeventdrivenmicroservices11728489736099.pdf
PDF
My first year with event sourcing amersfoort 12-06-2018
PDF
Event Sourcing with Kotlin, who needs frameworks!
PDF
Introduction to Domain-Driven Design in Ruby on Rails
PDF
My first year with event sourcing.dpc.
PDF
ZG PHP - Specification
Event sourcing with the GetEventStore
Rich domain model with symfony 2.5 and doctrine 2.5
Event sourcing w PHP (by Piotr Kacała)
My first year with event sourcing-nijmegen
Event Sourcing with php
From ActiveRecord to EventSourcing
My first year with event sourcing-symfonycon
Domain driven design
Domain-driven Design in PHP and Symfony - Drupal Camp Wroclaw!
Baby steps to Domain-Driven Design
Developing microservices with aggregates (melbourne)
Developing microservices with aggregates (devnexus2017)
Developing microservices with aggregates (SpringOne platform, #s1p)
Unbreakable Domain Models - DPC13
fundamentalsofeventdrivenmicroservices11728489736099.pdf
My first year with event sourcing amersfoort 12-06-2018
Event Sourcing with Kotlin, who needs frameworks!
Introduction to Domain-Driven Design in Ruby on Rails
My first year with event sourcing.dpc.
ZG PHP - Specification
Ad

More from Mathias Verraes (11)

PDF
Towards Modelling Processes
PDF
Modelling Heuristics
PDF
Small Controlled Experiments
PDF
Managed Technical Debt
PDF
DDD Basics: Bounded Contexts, Modelling - Kortrijk Edition
PDF
Why Domain-Driven Design Matters
PDF
Unbreakable Domain Models PHPUK 2014 London
PDF
Domain-Driven Design Basics
PDF
Model Storming Workshop PHP Benelux 2014
PDF
Fighting Bottlencks with CQRS - ResearchGate
PDF
DDDBE Modellathon 2013
Towards Modelling Processes
Modelling Heuristics
Small Controlled Experiments
Managed Technical Debt
DDD Basics: Bounded Contexts, Modelling - Kortrijk Edition
Why Domain-Driven Design Matters
Unbreakable Domain Models PHPUK 2014 London
Domain-Driven Design Basics
Model Storming Workshop PHP Benelux 2014
Fighting Bottlencks with CQRS - ResearchGate
DDDBE Modellathon 2013

Recently uploaded (20)

PDF
Advanced methodologies resolving dimensionality complications for autism neur...
PPT
Teaching material agriculture food technology
PPTX
20250228 LYD VKU AI Blended-Learning.pptx
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PDF
Review of recent advances in non-invasive hemoglobin estimation
PPTX
Digital-Transformation-Roadmap-for-Companies.pptx
PDF
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
PPTX
Spectroscopy.pptx food analysis technology
PPTX
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PPTX
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
PDF
Per capita expenditure prediction using model stacking based on satellite ima...
PPTX
Cloud computing and distributed systems.
PDF
Unlocking AI with Model Context Protocol (MCP)
PDF
Diabetes mellitus diagnosis method based random forest with bat algorithm
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PDF
Network Security Unit 5.pdf for BCA BBA.
PDF
Empathic Computing: Creating Shared Understanding
PPTX
Programs and apps: productivity, graphics, security and other tools
Advanced methodologies resolving dimensionality complications for autism neur...
Teaching material agriculture food technology
20250228 LYD VKU AI Blended-Learning.pptx
Reach Out and Touch Someone: Haptics and Empathic Computing
Review of recent advances in non-invasive hemoglobin estimation
Digital-Transformation-Roadmap-for-Companies.pptx
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
Spectroscopy.pptx food analysis technology
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
Agricultural_Statistics_at_a_Glance_2022_0.pdf
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
Per capita expenditure prediction using model stacking based on satellite ima...
Cloud computing and distributed systems.
Unlocking AI with Model Context Protocol (MCP)
Diabetes mellitus diagnosis method based random forest with bat algorithm
Building Integrated photovoltaic BIPV_UPV.pdf
Network Security Unit 5.pdf for BCA BBA.
Empathic Computing: Creating Shared Understanding
Programs and apps: productivity, graphics, security and other tools

Practical Event Sourcing

  • 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
  • 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
  • 14. Relevant to the business. ! First class citizens of the Domain Model
  • 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; } }
  • 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; } ! }
  • 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) {…}
  • 32. Possible outcomes ! nothing one or more events exception
  • 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); } !
  • 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
  • 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;
  • 53. The Event Store is an immutable, append-only database: infinite caching
  • 54. Querying events happens by aggregate id only
  • 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() ]);