Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Introduction to CQRS and Event Sourcing

1,368 views

Published on

Event Sourcing is the idea that every state of your application can be represented by a sequence of events. Using these two principles as the heart of a system or an application is quite common but can be challenging if we don’t use the right tools or architecture.

Published in: Engineering
  • Be the first to comment

Introduction to CQRS and Event Sourcing

  1. 1. IntroductiontoCQRS andEventSourcingSamuel ROZE (@samuelroze)
  2. 2. Theheartofsoftwareisitsability tosolvedomain-related problemsforitsuser — Eric Evans
  3. 3. EventStorming
  4. 4. EventSourcing
  5. 5. Ourdomain 1. Create a "deployment" 2. Start a deployment 3. Realtime status of our deployment
  6. 6. Ourmodelisthis... Anentity +------------------+ | Deployment | +------------------+ |- uuid: Uuid | |- sha1: string | |- status: string | |- startedBy: User | +------------------+
  7. 7. Whatwecouldhavedone ADoctrinemappingand..atable! +-------------------+ | deployment | +-------------------+ | uuid VARCHAR | | status VARCHAR | | sha1 VARCHAR | | startedBy VARCHAR | +-------------------+
  8. 8. Anotherapproach Asetofeventsasthereference! +-------------------------------------+ |DeploymentCreated |DeploymentStarted | | uuid: [...] | uuid: [...] | ... | datetime: [...] | datetime: [...] | | sha1: [...] | startedBy: [...] | +------------------+------------------+
  9. 9. Whyusingevents? 1. Closer to the business language 2. Keep the information about what happened 3. Easy to spread the logic across services 4. No coupling between domain and storage 5. Append-only it's a LOT easier to scale
  10. 10. Let'sgetstarted! Scenario: A deployment need to have a valid SHA-1 When I create a deployment for "123" Then the deployment should not be valid Scenario: Deployment for a valid SHA-1 When I create a deployment for "921103d" Then a deployment should be created
  11. 11. @When I create a deployment for :sha1 public function iCreateADeploymentFor(string $sha1) { try { $this->deployment = Deployment::create( Uuid::uuid4(), $sha1 ); } catch (Throwable $e) { $this->exception = $e; } }
  12. 12. @Then the deployment should not be valid public function theDeploymentShouldNotBeValid() { if (!$this->exception instanceof InvalidArgumentException) { throw new RuntimeException( 'The exception found, if any, is not matching' ); } }
  13. 13. @Then a deployment should be created public function aDeploymentShouldBeCreated() { $events = $this->deployment->raisedEvents(); $matchingEvents = array_filter($events, function(DeploymentEvent $event) { return $event instanceof DeploymentCreated; }); if (count($matchingEvents) === 0) { throw new RuntimeException('No deployment created found'); } }
  14. 14. Erm... $ bin/behat -fprogress FFFF 2 scenarios (0 passed) 4 steps (0 passed) 0m0.12s (40.89Mb)
  15. 15. Anevent interface DeploymentEvent { public function getDeploymentUuid() : UuidInterface; public function getDateTime(): DateTimeInterface; }
  16. 16. 'DeploymentCreated'event final class DeploymentCreated implements DeploymentEvent { public function __construct(UuidInterface $uuid, string $sha1) { /* .. */ } public function getDeploymentUuid() : UuidInterface { return $this->uuid; } public function getSha1() : string { return $this->sha1; } }
  17. 17. Eventcapability trait EventsCapability { private $events = []; protected function raise(DeploymentEvent $event) { $this->events[] = $event; } public function raisedEvents() : array { return $this->events; } }
  18. 18. Theaggregate final class Deployment { use EventsCapability; private function __construct() {} }
  19. 19. Creatingtheobjectfromevents final class Deployment { // ... public static function fromEvents(array $events) { $deployment = new self(); foreach ($events as $event) { $deployment->apply($event); } return $deployment; } }
  20. 20. Buildingtheobjectstate final class Deployment { private $uuid; // ... private function apply(DeploymentEvent $event) { if ($event instanceof DeploymentCreated) { $this->uuid = $event->getDeploymentUuid(); } } }
  21. 21. Create...fromthebeginning! final class Deployment { // ... public static function create(Uuid $uuid, string $sha1) { if (strlen($sha1) < 7) { throw new InvalidArgumentException('It is not a valid SHA-1'); } $createdEvent = new DeploymentCreated($uuid, $sha1); $deployment = self::fromEvents([$createdEvent]); $deployment->raise($createdEvent); return $deployment; } }
  22. 22. Wourah! $ bin/behat -fprogress .... 2 scenarios (2 passed) 4 steps (4 passed) 0m0.12s (40.89Mb)
  23. 23. Startingadeployment! Scenario: A successfully created deployment can be started Given a deployment was created When I start the deployment Then the deployment should be started Scenario: A deployment can be started only once Given a deployment was created and started When I start the deployment Then I should be told that the deployment has already started
  24. 24. @Given a deployment was created and started public function aDeploymentWasCreatedAndStarted() { $uuid = Uuid::uuid4(); $this->deployment = Deployment::fromEvents([ new DeploymentCreated($uuid, '921103d'), new DeploymentStarted($uuid), ]); }
  25. 25. @When I start the deployment public function iStartTheDeployment() { try { $this->deployment->start(); } catch (Throwable $e) { $this->exception = $e; } }
  26. 26. 'start'ingadeployment final class Deployment { private $uuid; private $started = false; // ... public function start() { if ($this->started) { throw new RuntimeException('Deployment already started'); } $this->raise(new DeploymentStarted($this->uuid)); } }
  27. 27. Keepingtraceofthestatus final class Deployment { private $started = false; // ... public function apply(DeploymentEvent $event) { // ... if ($event instanceof DeploymentStarted) { $this->started = true; } } }
  28. 28. That'stoofast... $ bin/behat -fprogress ......... 4 scenarios (4 passed) 10 steps (10 passed) 0m0.31s (41.22Mb)
  29. 29. Wearedone! ...withourdomain
  30. 30. Repositories&Persistence
  31. 31. EventStore interface EventStore { public function findByDeploymentUuid(UuidInterface $uuid) : array; public function add(DeploymentEvent $event); } Implementation detail: InMemory / Doctrine / Redis / GetEventStore / ...
  32. 32. Ourrepositorycontract interface DeploymentRepository { public function find(UuidInterface $uuid) : Deployment; }
  33. 33. Theevent-basedimplementation final class EventBasedDeploymentRepository implements DeploymentRepository { public function __construct(EventStore $eventStore) { /** .. **/ } public function find(UuidInterface $uuid) : Deployment { return Deployment::fromEvents( $this->eventStore->findByDeploymentUuid($uuid) ); } }
  34. 34. CQRS?
  35. 35. Projections! The"readmodel" · Creates a read-optimized view of our model · As many projection as you want · Any kind of backend (database, API, queue, ...)
  36. 36. final class DeploymentFirebaseProjector { public function __construct( DeploymentRepository $repository, FirebaseStorage $storage ) { /* ... */ } public function notify(DeploymentEvent $event) { $uuid = $event->getDeploymentUuid(); $deployment = $this->repository->find($uuid); $this->storage->store('deployments/'.$uuid, [ 'started' => $deployment->isStarted(), ]); } }
  37. 37. We'vedoneit! 1. Create a "deployment" 2. Start a deployment 3. Realtime status of our deployment
  38. 38. Thankyou! @samuelroze continuouspipe.io https://joind.in/talk/03af6
  39. 39. SimpleBus · Written by Matthias Noback http://simplebus.github.io/SymfonyBridge/ # app/config/config.yml event_bus: logging: ~ command_bus: logging: ~
  40. 40. final class DeploymentController { private $eventBus; public function __construct(MessageBus $eventBus) { /* ... */ } public function createAction(Request $request) { $deployment = Deployment::create( Uuid::uuid4(), $request->request->get('sha1') ); foreach ($deployment->raisedEvents() as $event) { $this->eventBus->handle($event); } return new Response(Response::HTTP_CREATED); } }
  41. 41. final class DeploymentController { private $commandBus; public function __construct(MessageBus $commandBus) { /* ... */ } public function createAction(Request $request) { $uuid = Uuid::uuid4(); $this->commandBus->handle(new CreateDeployment( $uuid, $request->request->get('sha1') )); return new Response(Response::HTTP_CREATED); } }
  42. 42. final class CreateDeploymentHandler { private $eventBus; public function __construct(MessageBus $eventBus) { /* ... */ } public function handle(CreateDeployment $command) { $deployment = Deployment::create( $command->getUuid(), $command->getSha1() ); foreach ($deployment->raisedEvents() as $event) { $this->eventBus->handle($event); } } }
  43. 43. Theplumbing <service id="app.controller.deployment" class="AppBundleControllerDeploymentController"> <argument type="service" id="command_bus" /> </service> <service id="app.handler.create_deployment" class="AppDeploymentHandlerCreateDeploymentHandler"> <argument type="service" id="event_bus" /> <tag name="command_handler" handles="AppCommandCreateDeployment" /> </service>
  44. 44. Whatdowehaverightnow? 1. Send a command from an HTTP API 2. The command handler talks to our domain 3. Domain raise an event 4. The event is dispatched to the event bus
  45. 45. Storingourevents final class DeploymentEventStoreMiddleware implements MessageBusMiddleware { private $eventStore; public function __construct(EventStore $eventStore) { $this->eventStore = $eventStore; } public function handle($message, callable $next) { if ($message instanceof DeploymentEvent) { $this->eventStore->add($message); } $next($message); } }
  46. 46. We<3XML <service id="app.event_bus.middleware.store_events" class="AppEventBusMiddlewareStoreEvents"> <argument type="service" id="event_store" /> <tag name="event_bus_middleware" /> </service>
  47. 47. Oureventsarestored! ...sowecangetour Deploymentfromthe repository
  48. 48. Let'sstartourdeployment! final class StartDeploymentWhenCreated { private $commandBus; public function __construct(MessageBus $commandBus) { /* ... */ } public function notify(DeploymentCreated $event) { // There will be conditions here... $this->commandBus->handle(new StartDeployment( $event->getDeploymentUuid() )); } }
  49. 49. Thehandler final class StartDeploymentHandler { public function __construct(DeploymentRepository $repository, MessageBus $eventBus) { /* ... */ } public function handle(StartDeployment $command) { $deployment = $this->repository->find($command->getDeploymentUuid()); $deployment->start(); foreach ($deployment->raisedEvents() as $event) { $this->eventBus->handle($event); } } }
  50. 50. Theplumbing <service id="app.deployment.auto_start.starts_when_created" class="AppDeploymentAutoStartStartsWhenCreated"> <argument type="service" id="command_bus" /> <tag name="event_subscriber" subscribes_to="AppEventDeploymentCreated" /> </service> <service id="app.deployment.handler.start_deployment" class="AppDeploymentHandlerStartDeploymentHandler"> <argument type="service" id="app.deployment_repository" /> <argument type="service" id="event_bus" /> <tag name="command_handler" handles="AppCommandStartDeployment" /> </service>
  51. 51. Whathappened? [...] 4. A dispatched DeploymentCreated event 5. A listener created a StartDeployment command 6. The command handler called the start method on the Deployment 7. The domain validated and raised a DeploymentStarted event 8. The DeploymentStarted was dispatched on
  52. 52. You'llgofurther...
  53. 53. final class Deployment { // ... public function finishedBuild(Build $build) { if ($build->isFailure()) { return $this->raise(new DeploymentFailed($this->uuid)); } $this->builtImages[] = $build->getImage(); if (count($this->builtImages) == count($this->images)) { $this->raise(new DeploymentSuccessful($this->uuid)); } } }
  54. 54. Dependencies...thewrongway final class Deployment { private $notifier; public function __construct(NotifierInterface $notifier) { /* .. */ } public function notify() { $this->notifier->notify($this); } }
  55. 55. Dependencies...therightway final class Deployment { public function notify(NotifierInterface $notifier) { $notifier->notify($this); } }
  56. 56. Testing!(layers) 1. Use your domain objects 2. Create commands and read your event store 3. Uses your API and projections
  57. 57. Whatwejustachieved 1. Incoming HTTP requests 2. Commands to the command bus 3. Handlers talk to your domain 4. Domain produces events 5. Events are stored and dispatched 6. Projections built for fast query
  58. 58. Thankyou! @samuelroze continuouspipe.io https://joind.in/talk/03af6

×