Top Banner
Introduction to CQRS and Event Sourcing Samuel ROZE (@samuelroze)
74

Introduction to CQRS and Event Sourcing

Apr 21, 2017

Download

Engineering

Samuel Rozé
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Introduction to CQRS and Event Sourcing

Introduction to CQRS and Event Sourcing

Samuel ROZE (@samuelroze)

Page 2: Introduction to CQRS and Event Sourcing
Page 3: Introduction to CQRS and Event Sourcing
Page 4: Introduction to CQRS and Event Sourcing
Page 5: Introduction to CQRS and Event Sourcing

The heart of software is its ability to solve domain-related

problems for its user— Eric Evans

Page 6: Introduction to CQRS and Event Sourcing
Page 7: Introduction to CQRS and Event Sourcing
Page 8: Introduction to CQRS and Event Sourcing
Page 9: Introduction to CQRS and Event Sourcing

Event Storming

Page 10: Introduction to CQRS and Event Sourcing

Event Sourcing

Page 11: Introduction to CQRS and Event Sourcing

Our domain

1. Create a "deployment"

2. Start a deployment

3. Realtime status of our deployment

Page 12: Introduction to CQRS and Event Sourcing

Our model is this...

An entity +------------------+ | Deployment | +------------------+ |- uuid: Uuid | |- sha1: string | |- status: string | |- startedBy: User | +------------------+

Page 13: Introduction to CQRS and Event Sourcing

What we could have doneA Doctrine mapping and.. a table!

+-------------------+ | deployment | +-------------------+ | uuid VARCHAR | | status VARCHAR | | sha1 VARCHAR | | startedBy VARCHAR | +-------------------+

Page 14: Introduction to CQRS and Event Sourcing

An other approachA set of events as the reference!

+-------------------------------------+ |DeploymentCreated |DeploymentStarted | | uuid: [...] | uuid: [...] | ... | datetime: [...] | datetime: [...] | | sha1: [...] | startedBy: [...] | +------------------+------------------+

Page 15: Introduction to CQRS and Event Sourcing

Why using events?1. Closer to the business language

2. Keep the information about what happened3. Easy to spread the logic across services

4. No coupling between domain and storage5. Append-only it's a LOT easier to scale

Page 16: Introduction to CQRS and Event Sourcing
Page 17: Introduction to CQRS and Event Sourcing

Let's get started !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

Page 18: Introduction to CQRS and Event Sourcing

@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; }}

Page 19: Introduction to CQRS and Event Sourcing

@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' ); }}

Page 20: Introduction to CQRS and Event Sourcing

@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'); }}

Page 21: Introduction to CQRS and Event Sourcing

Erm...$ bin/behat -fprogressFFFF

2 scenarios (0 passed)4 steps (0 passed)0m0.12s (40.89Mb)

Page 22: Introduction to CQRS and Event Sourcing

An eventinterface DeploymentEvent{ public function getDeploymentUuid() : UuidInterface;

public function getDateTime(): \DateTimeInterface;}

Page 23: Introduction to CQRS and Event Sourcing

'DeploymentCreated' eventfinal 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; }}

Page 24: Introduction to CQRS and Event Sourcing

Event capabilitytrait EventsCapability{ private $events = [];

protected function raise(DeploymentEvent $event) { $this->events[] = $event; }

public function raisedEvents() : array { return $this->events; }}

Page 25: Introduction to CQRS and Event Sourcing

The aggregatefinal class Deployment{ use EventsCapability;

private function __construct() {}}

Page 26: Introduction to CQRS and Event Sourcing

Creating the object from eventsfinal class Deployment{ // ... public static function fromEvents(array $events) { $deployment = new self();

foreach ($events as $event) { $deployment->apply($event); }

return $deployment; }}

Page 27: Introduction to CQRS and Event Sourcing

Building the object statefinal class Deployment{ private $uuid;

// ...

private function apply(DeploymentEvent $event) { if ($event instanceof DeploymentCreated) { $this->uuid = $event->getDeploymentUuid(); } }}

Page 28: Introduction to CQRS and Event Sourcing

Create... from the beginning!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; }}

Page 29: Introduction to CQRS and Event Sourcing

Wourah!$ bin/behat -fprogress....

2 scenarios (2 passed)4 steps (4 passed)0m0.12s (40.89Mb)

Page 30: Introduction to CQRS and Event Sourcing
Page 31: Introduction to CQRS and Event Sourcing

Starting a deployment !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

Page 32: Introduction to CQRS and Event Sourcing

@Given a deployment was created and started

public function aDeploymentWasCreatedAndStarted(){ $uuid = Uuid::uuid4();

$this->deployment = Deployment::fromEvents([ new DeploymentCreated($uuid, '921103d'), new DeploymentStarted($uuid), ]);}

Page 33: Introduction to CQRS and Event Sourcing

@When I start the deployment

public function iStartTheDeployment(){ try { $this->deployment->start(); } catch (\Throwable $e) { $this->exception = $e; }}

Page 34: Introduction to CQRS and Event Sourcing

'start'ing a deploymentfinal 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)); }}

Page 35: Introduction to CQRS and Event Sourcing

Keeping trace of the statusfinal class Deployment{ private $started = false; // ...

public function apply(DeploymentEvent $event) { // ... if ($event instanceof DeploymentStarted) { $this->started = true; } }}

Page 36: Introduction to CQRS and Event Sourcing

That's too fast...$ bin/behat -fprogress.........

4 scenarios (4 passed)10 steps (10 passed)0m0.31s (41.22Mb)

Page 37: Introduction to CQRS and Event Sourcing

We are done!...with our domain

Page 38: Introduction to CQRS and Event Sourcing

Repositories & Persistence

Page 39: Introduction to CQRS and Event Sourcing

Event Storeinterface EventStore{ public function findByDeploymentUuid(UuidInterface $uuid) : array;

public function add(DeploymentEvent $event);}

Implementation detail: InMemory / Doctrine / Redis / GetEventStore / ...

Page 40: Introduction to CQRS and Event Sourcing

Our repository contractinterface DeploymentRepository{ public function find(UuidInterface $uuid) : Deployment;}

Page 41: Introduction to CQRS and Event Sourcing

The event-based implementationfinal class EventBasedDeploymentRepository implements DeploymentRepository{ public function __construct(EventStore $eventStore) { /** .. **/ }

public function find(UuidInterface $uuid) : Deployment { return Deployment::fromEvents( $this->eventStore->findByDeploymentUuid($uuid) ); }}

Page 42: Introduction to CQRS and Event Sourcing

CQRS?

Page 43: Introduction to CQRS and Event Sourcing
Page 44: Introduction to CQRS and Event Sourcing
Page 45: Introduction to CQRS and Event Sourcing
Page 46: Introduction to CQRS and Event Sourcing
Page 47: Introduction to CQRS and Event Sourcing
Page 48: Introduction to CQRS and Event Sourcing
Page 49: Introduction to CQRS and Event Sourcing
Page 50: Introduction to CQRS and Event Sourcing
Page 51: Introduction to CQRS and Event Sourcing

Projections!The "read model"

· Creates a read-optimized view of our model· As many projection as you want

· Any kind of backend (database, API, queue, ...)

Page 52: Introduction to CQRS and Event Sourcing

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(), ]); }}

Page 53: Introduction to CQRS and Event Sourcing

We've done it!1. Create a "deployment"

2. Start a deployment

3. Realtime status of our deployment

Page 54: Introduction to CQRS and Event Sourcing

Thank you!@samuelroze

continuouspipe.iohttps://joind.in/talk/03af6

Page 55: Introduction to CQRS and Event Sourcing

SimpleBus· Written by Matthias Noback

http://simplebus.github.io/SymfonyBridge/

# app/config/config.ymlevent_bus: logging: ~

command_bus: logging: ~

Page 56: Introduction to CQRS and Event Sourcing

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); }}

Page 57: Introduction to CQRS and Event Sourcing

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); }}

Page 58: Introduction to CQRS and Event Sourcing

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); } }}

Page 59: Introduction to CQRS and Event Sourcing

The plumbing<service id="app.controller.deployment" class="AppBundle\Controller\DeploymentController"> <argument type="service" id="command_bus" /></service>

<service id="app.handler.create_deployment" class="App\Deployment\Handler\CreateDeploymentHandler"> <argument type="service" id="event_bus" />

<tag name="command_handler" handles="App\Command\CreateDeployment" /></service>

Page 60: Introduction to CQRS and Event Sourcing

What do we have right now?1. Send a command from an HTTP API

2. The command handler talks to our domain3. Domain raise an event

4. The event is dispatched to the event bus

Page 61: Introduction to CQRS and Event Sourcing

Storing our eventsfinal 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); }}

Page 62: Introduction to CQRS and Event Sourcing

We <3 XML<service id="app.event_bus.middleware.store_events" class="App\EventBus\Middleware\StoreEvents"> <argument type="service" id="event_store" />

<tag name="event_bus_middleware" /></service>

Page 63: Introduction to CQRS and Event Sourcing

Our events are stored!...so we can get our

Deployment from the repository

Page 64: Introduction to CQRS and Event Sourcing

Let's start our deployment!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() )); }}

Page 65: Introduction to CQRS and Event Sourcing

The handlerfinal 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); } }}

Page 66: Introduction to CQRS and Event Sourcing

The plumbing<service id="app.deployment.auto_start.starts_when_created" class="App\Deployment\AutoStart\StartsWhenCreated"> <argument type="service" id="command_bus" />

<tag name="event_subscriber" subscribes_to="App\Event\DeploymentCreated" /></service>

<service id="app.deployment.handler.start_deployment" class="App\Deployment\Handler\StartDeploymentHandler"> <argument type="service" id="app.deployment_repository" /> <argument type="service" id="event_bus" />

<tag name="command_handler" handles="App\Command\StartDeployment" /></service>

Page 67: Introduction to CQRS and Event Sourcing

What happened?[...]

4. A dispatched DeploymentCreated event5. A listener created a StartDeployment

command6. The command handler called the start

method on the Deployment7. The domain validated and raised a

DeploymentStarted event8. The DeploymentStarted was dispatched on

Page 68: Introduction to CQRS and Event Sourcing

You'll go further...

Page 69: Introduction to CQRS and Event Sourcing

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)); } }}

Page 70: Introduction to CQRS and Event Sourcing

Dependencies... the wrong wayfinal class Deployment{ private $notifier;

public function __construct(NotifierInterface $notifier) { /* .. */ }

public function notify() { $this->notifier->notify($this); }}

Page 71: Introduction to CQRS and Event Sourcing

Dependencies... the right wayfinal class Deployment{ public function notify(NotifierInterface $notifier) { $notifier->notify($this); }}

Page 72: Introduction to CQRS and Event Sourcing

Testing! (layers)1. Use your domain objects

2. Create commands and read your event store3. Uses your API and projections

Page 73: Introduction to CQRS and Event Sourcing

What we just achieved1. Incoming HTTP requests

2. Commands to the command bus3. Handlers talk to your domain

4. Domain produces events5. Events are stored and dispatched

6. Projections built for fast query

Page 74: Introduction to CQRS and Event Sourcing

Thank you!@samuelroze

continuouspipe.iohttps://joind.in/talk/03af6