Time traveling with Event Sourcing

Time traveling with Event Sourcing

Introduction to Event Sourcing presented on PHPCon Poland 2016.

A96d84df0644f1a41f47c5a0b7c38981?s=128

Olaf Gałązka

October 01, 2016
Tweet

Transcript

  1. Time traveling
 with Event Sourcing Olaf Gałązka

  2. None
  3. None
  4. None
  5. Traditional way • CRUD • current state based

  6. Current state concept • saving only state of all objects

    • no ways to investigate what was happening
  7. Before After

  8. Before After

  9. Workaround? Saving history records into log

  10. None
  11. None
  12. None
  13. Good solution?

  14. None
  15. Switch your mindset

  16. Focus on actions that happen in your application

  17. The domain events

  18. The domain event • a fact • something that happened

    in the past • a state transition
  19. Event = data • event keeps all information about itself

    • it’s the main and only source of information • think of all data needed
  20. Events naming • if events are facts from the past

    • then use verbs • and use past tense
  21. Events naming • ArticleWasPublished • ProductWasAddedToBasket • NewUserHasRegistered

  22. None
  23. https://github.com/ulff/phpcon-demo

  24. 
 class PostWasPublished implements DomainEvent
 {
 private $postId;
 
 private

    $title;
 
 private $content;
 
 private $publishingDate;
 
 public function __construct(
 PostId $postId,
 $title,
 $content,
 \DateTime $publishingDate
 )
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->content = $content;
 $this->publishingDate = $publishingDate;
 }
 
 // ...
 }
  25. Storing events

  26. Event storage • persistence layer for events • append only

    • never ever delete nor update
  27. Stored event • event type • aggregate id • domain

    event metadata • optional useful information, e.g. date
  28. Aggregates • objects that are affected by events • it’s

    like a document / entity
  29. Post PostWasPublished PostWasUpdated Event Event Aggregate

  30. interface Aggregate
 {
 /**
 * @return DomainEvent[]
 */
 public function

    getEvents();
 
 /**
 * @return AggregateId
 */
 public function getAggregateId();
 
 /**
 * @return Aggregate
 */
 public static function reconstituteFrom( AggregateHistory $aggregateHistory );
 }
  31. class Post implements Aggregate
 {
 use EventSourced;
 
 private $postId;


    private $title;
 private $content;
 private $publishingDate;
 
 public function __construct(PostId $postId) {/**/}
 
 // getters/setters… 
 public function getAggregateId() {/**/}
 public function update($title, $content) {/**/}
 private function applyPostWasPublished(PostWasPublished $event) {/**/}
 private function applyPostWasUpdated(PostWasUpdated $event) {/**/}
 public static function create($title, $content) {/**/}
 public static function reconstituteFrom( AggregateHistory $postAggregateHistory ) {/**/}
 }
  32. Product ProductWasAddedToBasket Basket What is an aggregate here? ?

  33. Product ProductWasAddedToBasket Basket Basket is the affected object

  34. Aggregate ID • every aggregate has defined its aggregateID •

    every event has defined aggregateID
  35. 
 class PostWasPublished implements DomainEvent
 {
 private $postId;
 
 private

    $title;
 
 private $content;
 
 private $publishingDate;
 
 public function __construct(
 PostId $postId,
 $title,
 $content,
 \DateTime $publishingDate
 )
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->content = $content;
 $this->publishingDate = $publishingDate;
 }
 
 // ...
 }
  36. class PostWasUpdated implements DomainEvent
 {
 private $postId;
 
 private $title;


    
 private $content;
 
 public function __construct(
 PostId $postId,
 $title,
 $content
 )
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->content = $content;
 }
 
 // ...
 }
  37. Reconstituting aggregates • get aggregate history from the event storage

    • aggregate history is an ordered set of events affecting particular aggregate • reconstitute aggregate’s state: ‣ create new instance of the aggregate class ‣ replay events from aggregate history on it
  38. class Post implements Aggregate
 {
 public function __construct(PostId $postId)
 {


    $this->postId = $postId;
 } 
 
 // ...
 
 public static function reconstituteFrom(PostAggregateHistory $aggregateHistory)
 {
 $post = new self($aggregateHistory->getAggregateId());
 
 foreach ($aggregateHistory->getEvents() as $event) {
 $applyMethod = explode('\\', get_class($event));
 $applyMethod = 'apply' . end($applyMethod);
 $post->$applyMethod($event);
 }
 
 return $post;
 }
 }
  39. class Post implements Aggregate
 {
 // ...
 
 private function

    applyPostWasPublished(PostWasPublished $event)
 {
 $this->setTitle($event->getTitle());
 $this->setContent($event->getContent());
 $this->setPublishingDate($event->getPublishingDate());
 }
 
 private function applyPostWasUpdated(PostWasUpdated $event)
 {
 $this->setTitle($event->getTitle());
 $this->setContent($event->getContent());
 }
 
 // ... 
 }
  40. Reconstituted aggregate • use reconstituted object in your operations •

    never save it’s state • let the garbage collector remove it
  41. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post);
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 }
  42. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post);
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 } $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
  43. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post);
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 } $post->update(
 $command->getTitle(),
 $command->getContent()
 );
  44. class Post implements Aggregate
 {
 use EventSourced;
 
 // ...


    public function update($title, $content)
 {
 $this->recordThat($event = new PostWasUpdated( $this->getAggregateId(), $title, $content ));
 $this->apply($event);
 }
 
 private function applyPostWasUpdated(PostWasUpdated $event)
 {
 $this->setTitle($event->getTitle());
 $this->setContent($event->getContent());
 }
 // ...
 }
  45. trait EventSourced
 {
 protected $events = [];
 
 public function

    getEvents()
 {
 return $this->events;
 }
 
 protected function recordThat(DomainEvent $event)
 {
 $this->events[] = $event;
 }
 
 private function apply(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'apply' . end($method);
 $this->$method($event);
 }
 
 // ...
 }
  46. // class Domain\UseCase\UpdatePost 
 public function execute(Command $command, Responder $responder)


    {
 $postAggregateHistory = new PostAggregateHistory(
 $command->getPostId(), 
 $this->eventStorage
 );
 $post = Post::reconstituteFrom($postAggregateHistory);
 
 $post->update(
 $command->getTitle(),
 $command->getContent()
 );
 
 try {
 $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post->getEvents());
 } catch(\Exception $e) {
 $responder->postUpdatingFailed($e);
 }
 
 $responder->postUpdatedSuccessfully($post);
 } $this->eventBus->dispatch($post->getEvents());
 $this->eventStorage->add($post->getEvents());
  47. Dispatching events

  48. When event was produced • it should be saved in

    the event storage • it should be dispatched • so any interested listeners could use it
  49. class EventBus
 {
 private $listeners = [];
 
 public function

    registerListener(
 DomainEventListener $domainEventListener
 )
 {
 $this->listeners[] = $domainEventListener;
 }
 
 public function dispatch(array $events)
 {
 foreach ($events as $event) {
 foreach ($this->listeners as $listener) {
 $listener->when($event);
 }
 }
 }
 }
  50. interface DomainEventListener
 {
 /**
 * @param DomainEvent $event
 */
 public

    function when(DomainEvent $event);
 }
  51. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 }
  52. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 } $eventBus->registerListener($this);
  53. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 } $this->projectionStorage = $projectionStorage;
  54. Read model

  55. Projecting Projecting is process of converting (or aggregating) a stream

    of events into a structural representation - persistent read model.
  56. Projections • data stored in the way it can be

    easily used for reading purposes • very simple structure • updated on every event affecting any of its data
  57. class PostListProjection implements Projection
 {
 const PROJECTION_NAME = 'post-list';
 private

    $postId;
 public $title;
 public $publishingDate;
 
 public function __construct(PostId $postId, $title, \DateTime $publishingDate)
 {
 $this->postId = $postId;
 $this->title = $title;
 $this->publishingDate = $publishingDate;
 }
 
 public function getProjectionName()
 {
 return self::PROJECTION_NAME;
 }
 
 public function getAggregateId()
 {
 return $this->postId;
 }
 // ...
 }
  58. Projections • persisted (db, files, cache) • can always be

    cleared • and then can be replayed from event stream
  59. Projections • projections are extremely fast to load • possible

    minimal latency of update • eventually consistent with event storage
  60. abstract class AbstractDomainEventListener implements DomainEventListener
 {
 protected $projectionStorage;
 
 public

    function __construct( EventBus $eventBus, ProjectionStorage $projectionStorage )
 {
 $eventBus->registerListener($this);
 $this->projectionStorage = $projectionStorage;
 }
 
 public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
 } public function when(DomainEvent $event)
 {
 $method = explode('\\', get_class($event));
 $method = 'on' . end($method);
 if (method_exists($this, $method)) {
 $this->$method($event);
 }
 }
  61. class PostListListener 
 extends AbstractDomainEventListener 
 implements DomainEventListener
 {
 public

    function onPostWasPublished(PostWasPublished $event)
 {
 $postListProjection = new PostListProjection(
 $event->getAggregateId(),
 $event->getTitle(),
 $event->getPublishingDate()
 );
 $this->projectionStorage->save($postListProjection);
 }
 
 public function onPostWasUpdated(PostWasUpdated $event)
 {
 $postListProjection = $this->projectionStorage->findById(
 'post-list',
 $event->getAggregateId()
 );
 $postListProjection->title = $event->getTitle();
 $postListProjection->content = $event->getContent();
 $this->projectionStorage->save($postListProjection);
 }
 }
  62. CQRS Command Query Responsibility Segregation

  63. –Greg Young „You can use CQRS without event sourcing, but

    with event sourcing you must use CQRS.”
  64. CQRS in event sourcing UI Command (write) Query (read) Event

    Model Event Storage Projection Storage Event Bus save() dispatch() Read Model Listeners Transaction scope
  65. Performance

  66. Performance: read model • projections are extremely fast to read

    • possible minimal latency on updating • dispatching is an only risk in some cases
  67. Performance: event storage • append only, immutable • immutable data

    is very easy to scale up • can handle millions of events • but you can always make a snapshot
  68. Snapshots • allow reconstituting object not from zero state •

    to avoid processing large numbers of events
  69. 1 2 n n+1 n+2 n+3 n+4 n+5 n+6 n+7

    … Reconstituting Snapshots
  70. 1 2 n n+1 n+2 n+3 n+4 n+5 n+6 n+7

    … snapshot Reconstituting Snapshots
  71. None
  72. None
  73. Is event sourcing better model?

  74. None
  75. Event sourcing • simplifies modelling • focuses on facts and

    actions that happen • gives better overview
  76. Better overview

  77. Event sourced application is more real-life like

  78. –Greg Young „A state transition is not Update table set

    column equals value. It’s a fact”
  79. Event source is used • in banking • in finance

    • in insurance • in bookkeeping
  80. With event sourcing you can • go back to any

    point of time • analyze and compare data from any periods • you can delete and replay any read model data
  81. With event sourcing you will avoid • deleting something accidentally

    • problems with missing data that could have been saved • hard investigations of what has happened
  82. None
  83. None
  84. BasketWasPickedUp ProductWasAddedToBasket ProductWasAddedToBasket ProductWasRemovedFromBasket BasketWasCheckedOut

  85. BasketWasPickedUp ProductWasAddedToBasket ProductWasAddedToBasket ProductWasRemovedFromBasket BasketWasCheckedOut BasketWasPickedUp ProductWasAddedToBasket BasketWasCheckedOut

  86. Software

  87. geteventstore.com • high performance event storage • a plenty of

    additional features • introduced in 2012 by Greg Young, still developed
  88. candidate-labs / broadway • PHP solution • https://github.com/qandidate-labs/broadway

  89. prooph • PHP solution • http://getprooph.org/ • https://github.com/prooph

  90. TL;DR Event sourcing principles • do not store state of

    objects • store only facts that happened (events) • reconstitute state of aggregates from event storage • dispatch events to update projections • use projections for reading
  91. Questions?

  92. Thank you! Presented sources are part of demo application: https://github.com/ulff/phpcon-demo