Introduction to CQRS and Event Sourcing
-
Upload
samuel-roze -
Category
Engineering
-
view
561 -
download
5
Transcript of Introduction to CQRS and Event Sourcing
Introduction to CQRS and Event Sourcing
Samuel ROZE (@samuelroze)
The heart of software is its ability to solve domain-related
problems for its user— Eric Evans
Event Storming
Event Sourcing
Our domain
1. Create a "deployment"
2. Start a deployment
3. Realtime status of our deployment
Our model is this...
An entity +------------------+ | Deployment | +------------------+ |- uuid: Uuid | |- sha1: string | |- status: string | |- startedBy: User | +------------------+
What we could have doneA Doctrine mapping and.. a table!
+-------------------+ | deployment | +-------------------+ | uuid VARCHAR | | status VARCHAR | | sha1 VARCHAR | | startedBy VARCHAR | +-------------------+
An other approachA set of events as the reference!
+-------------------------------------+ |DeploymentCreated |DeploymentStarted | | uuid: [...] | uuid: [...] | ... | datetime: [...] | datetime: [...] | | sha1: [...] | startedBy: [...] | +------------------+------------------+
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
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
@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; }}
@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' ); }}
@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'); }}
Erm...$ bin/behat -fprogressFFFF
2 scenarios (0 passed)4 steps (0 passed)0m0.12s (40.89Mb)
An eventinterface DeploymentEvent{ public function getDeploymentUuid() : UuidInterface;
public function getDateTime(): \DateTimeInterface;}
'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; }}
Event capabilitytrait EventsCapability{ private $events = [];
protected function raise(DeploymentEvent $event) { $this->events[] = $event; }
public function raisedEvents() : array { return $this->events; }}
The aggregatefinal class Deployment{ use EventsCapability;
private function __construct() {}}
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; }}
Building the object statefinal class Deployment{ private $uuid;
// ...
private function apply(DeploymentEvent $event) { if ($event instanceof DeploymentCreated) { $this->uuid = $event->getDeploymentUuid(); } }}
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; }}
Wourah!$ bin/behat -fprogress....
2 scenarios (2 passed)4 steps (4 passed)0m0.12s (40.89Mb)
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
@Given a deployment was created and started
public function aDeploymentWasCreatedAndStarted(){ $uuid = Uuid::uuid4();
$this->deployment = Deployment::fromEvents([ new DeploymentCreated($uuid, '921103d'), new DeploymentStarted($uuid), ]);}
@When I start the deployment
public function iStartTheDeployment(){ try { $this->deployment->start(); } catch (\Throwable $e) { $this->exception = $e; }}
'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)); }}
Keeping trace of the statusfinal class Deployment{ private $started = false; // ...
public function apply(DeploymentEvent $event) { // ... if ($event instanceof DeploymentStarted) { $this->started = true; } }}
That's too fast...$ bin/behat -fprogress.........
4 scenarios (4 passed)10 steps (10 passed)0m0.31s (41.22Mb)
We are done!...with our domain
Repositories & Persistence
Event Storeinterface EventStore{ public function findByDeploymentUuid(UuidInterface $uuid) : array;
public function add(DeploymentEvent $event);}
Implementation detail: InMemory / Doctrine / Redis / GetEventStore / ...
Our repository contractinterface DeploymentRepository{ public function find(UuidInterface $uuid) : Deployment;}
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) ); }}
CQRS?
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, ...)
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(), ]); }}
We've done it!1. Create a "deployment"
2. Start a deployment
3. Realtime status of our deployment
Thank you!@samuelroze
continuouspipe.iohttps://joind.in/talk/03af6
SimpleBus· Written by Matthias Noback
http://simplebus.github.io/SymfonyBridge/
# app/config/config.ymlevent_bus: logging: ~
command_bus: logging: ~
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); }}
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); }}
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); } }}
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>
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
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); }}
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>
Our events are stored!...so we can get our
Deployment from the repository
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() )); }}
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); } }}
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>
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
You'll go further...
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)); } }}
Dependencies... the wrong wayfinal class Deployment{ private $notifier;
public function __construct(NotifierInterface $notifier) { /* .. */ }
public function notify() { $this->notifier->notify($this); }}
Dependencies... the right wayfinal class Deployment{ public function notify(NotifierInterface $notifier) { $notifier->notify($this); }}
Testing! (layers)1. Use your domain objects
2. Create commands and read your event store3. Uses your API and projections
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
Thank you!@samuelroze
continuouspipe.iohttps://joind.in/talk/03af6