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.

When cqrs meets event sourcing

Have you ever wondered how all this CQRS / ES concepts could be applied to a PHP project? Let's take a look at some code of our last project: a warehouse management system.

This talk will show how we understand DDD and how we apply it at Ulabox, what changes in application's architecture and code when we apply CQRS and how to deal with event sourcing, when there're no persisted entities, only events that generate projections used in the read model.

Speaker: Manel Sellés (@manelselles), software engineer, DDD-TDD fan and Symfony Expert Certified by Sensiolabs. Currently developing enterprise logistics software at Ulabox.com

  • Be the first to comment

When cqrs meets event sourcing

  1. 1. When CQRS meets Event Sourcing A warehouse management system done in PHP
  2. 2. When CQRS meets Event Sourcing / Ulabox ULABOX
  3. 3. About me ● @manelselles ● Backend at Ulabox ● Symfony Expert Certified by Sensiolabs ● DDD-TDD fan
  4. 4. When CQRS meets Event Sourcing / Warehouse Warehouse management system ● PHP and framework agnostic ○ (almost) all of us love Symfony ● Independent of other systems ○ Ulabox ecosystem is complex -> Microservices ● Extensible and maintainable ○ Testing ● The system must log every action ○ Event driven architecture
  5. 5. When CQRS meets Event Sourcing / Warehouse
  6. 6. Please test! Good practices
  7. 7. When CQRS meets Event Sourcing / Good practices Outside-in TDD ● Behat features ● Describe behaviour with PhpSpec ● Testing integration with database of repository methods with Phpunit
  8. 8. When CQRS meets Event Sourcing / Good practices Continuous integration
  9. 9. When CQRS meets Event Sourcing / Good practices Other good practices ● SOLID ● Coding Style ● Pair programming ● Refactor
  10. 10. DDD-Hexagonal architecture
  11. 11. When CQRS meets Event Sourcing / DDD-Hexagonal DDD Basics ● Strategic ○ Ubiquitous language ○ Bounded contexts ● Tactical ○ Value objects ○ Aggregates and entities ○ Repositories ○ Domain events ○ Domain and application services
  12. 12. When CQRS meets Event Sourcing / DDD-Hexagonal Aggregate
  13. 13. When CQRS meets Event Sourcing / DDD-Hexagonal Hexagonal architecture
  14. 14. namespace UlaboxChangoInfrastructureUiHttpController; class ReceptionController { public function addContainerAction(JsonApiRequest $request, $receptionId) { $containerPayload = $this->jsonApiTransformer->fromPayload($request->jsonData(), 'container'); $this->receptionService->addContainer(ReceptionId::fromString($receptionId), $containerPayload); return JsonApiResponse::createJsonApiData(200, null, []); } } namespace UlaboxChangoInfrastructureUiAmqpConsumer; class ContainerAddedToReceptionConsumer extends Consumer { public function execute(AMQPMessage $rabbitMessage) { $message = $this->messageBody($rabbitMessage); $containerPayload = $this->amqpTransformer->fromPayload($message, 'container'); $this->receptionService->addContainer(ReceptionId::fromString($message['reception_id']), $containerPayload); return ConsumerInterface::MSG_ACK; } }
  15. 15. namespace UlaboxChangoApplicationService; class ReceptionService { public function addContainer(ReceptionId $receptionId, ContainerPayload $payload) { $reception = $this->receptionRepository->get($receptionId); $reception->addContainer($payload->temperature(), $payload->lines()); $this->receptionRepository->save($reception); $this->eventBus->dispatch($reception->recordedEvents()); } }
  16. 16. When CQRS meets Event Sourcing / DDD-Hexagonal Why application service? ● Same entry point ● Coordinate tasks on model ● Early checks ● User authentication
  17. 17. namespace UlaboxChangoDomainModelReception; class Reception extends Aggregate { public function addContainer(Temperature $temperature, array $containerLines) { Assertion::allIsInstanceOf($containerLines, ContainerLinePayload::class); $containerId = ContainerId::create($this->id(), $temperature, count($this->containers)); $this->containers->set((string) $containerId, new Container($containerId, $temperature)); $this->recordThat(new ContainerWasAdded($this->id, $containerId, $temperature)); foreach ($containerLines as $line) { $this->addLine($containerId, $line->label(), $line->quantity(), $line->type()); } } public function addLine(ContainerId $containerId, Label $label, LineQuantity $quantity, ItemType $type) { if (!$container = $this->containers->get((string) $containerId)) { throw new EntityNotFoundException("Container not found"); } $container->addLine(ContainerLine::create($label, $quantity, $type)); $this->recordThat(new ContainerLineWasAdded($this->id, $containerId, $label, $quantity, $type)); } }
  18. 18. namespace UlaboxChangoDomainModelReceptionContainer; class Container { public function __construct(ContainerId $id, Temperature $temperature) { $this->id = $id; $this->temperature = $temperature; $this->lines = new ArrayCollection(); $this->status = ContainerStatus::PENDING(); } public function addLine(ContainerLine $line) { if ($this->containsLine($line->label())) { throw new AlreadyRegisteredException("Line already exists"); } $this->lines->set((string) $line->label(), $line); } }
  19. 19. namespace UlaboxChangoInfrastructurePersistenceDoctrineReception; class DoctrineReceptionRepository implements ReceptionRepository { public function get(ReceptionId $id) { return $this->find($id); } public function save(Reception $reception) { $this->_em->persist($reception); } }
  20. 20. Let’s apply Command and Query Responsibility Segregation
  21. 21. When CQRS meets Event Sourcing / CQRS CQRS Separate: ● Command: do something ● Query: ask for something Different source of data for read and write: ● Write model with DDD tactical patterns ● Read model with listeners to events
  22. 22. When CQRS meets Event Sourcing / CQRS Command bus ● Finds handler for each action ● Decoupled command creator and handler ● Middlewares ○ Transactional ○ Logging ● Asynchronous actions ● Separation of concerns
  23. 23. When CQRS meets Event Sourcing / CQRS Event bus ● Posted events are delivered to matching event handlers ● Decouples event producers and reactors ● Middlewares ○ Rabbit ○ Add correlation id ● Asynchronous actions ● Separation of concerns
  24. 24. When CQRS meets Event Sourcing / CQRS
  25. 25. namespace UlaboxChangoApplicationService; class ReceptionService { public function addContainer(ReceptionId $receptionId, ContainerPayload $payload) { $command = new AddContainer($receptionId, $payload->temperature(), $payload->containerLines()); $this->commandBus->handle($command); } } namespace UlaboxChangoDomainCommandReception; class ReceptionCommandHandler extends CommandHandler { public function handleAddContainer(AddContainer $command) { $reception = $this->receptionRepository->get($command->aggregateId()); $reception->addContainer($command->temperature(), $command->lines()); $this->receptionRepository->save($reception); $this->eventBus->dispatch($reception->recordedEvents()); } }
  26. 26. namespace UlaboxChangoDomainReadModelReception; class ReceptionProjector extends ReadModelProcessor { public function applyContainerWasAdded(ContainerWasAdded $event) { $reception = $this->receptionInfoView->receptionOfId($event->aggregateId()); $container = new ContainerProjection($event->containerId(), $event->temperature()); $this->receptionInfoView->save($reception->addContainer($container)); } public function applyContainerLineWasAdded(ContainerLineWasAdded $event) { $reception = $this->receptionInfoView->receptionOfId($event->aggregateId()); $line = ContainerLineProjection($event->label(), $event->quantity(), $event->itemType()); $this->receptionInfoView->save($reception->addContainerLine($event->containerId(), $line)); } } namespace UlaboxChangoDomainReadModelReception; interface ReceptionView { public function save(ReceptionProjection $reception); public function receptionOfId(ReceptionId $receptionId); public function find(Query $query); }
  27. 27. namespace UlaboxChangoApplicationService; class ReceptionQueryService { public function byId(ReceptionId $receptionId) { return $this->receptionView->receptionOfId($receptionId); } public function byContainer(ContainerId $containerId) { return $this->receptionView->find(new byContainer($containerId)); } public function search($filters, Paging $paging = null, Sorting $sorting = null) { return $this->receptionView->find(new ByFilters($filters, $sorting, $paging)); } }
  28. 28. Let’s get crazy: event sourcing
  29. 29. When CQRS meets Event Sourcing / Event sourcing Event sourcing ● Entities are reconstructed with events ● No state ● No database to update manually ● No joins
  30. 30. When CQRS meets Event Sourcing / Event sourcing Why event sourcing? ● Get state of an aggregate at any moment in time ● Append-only model storing events is easier to scale ● Forces to log because everything is an event ● No coupling between current state in the domain and in storage ● Simulate business suppositions ○ Change picking algorithm
  31. 31. When CQRS meets Event Sourcing / Event sourcing Event Store ● PostgreSQL ● jsonb ● DBAL
  32. 32. namespace UlaboxChangoInfrastructurePersistenceEventStore; class PDOEventStore implements EventStore { public function append(AggregateId $id, EventStream $eventStream) { $stmt = $this->connection->prepare("INSERT INTO event_store (data) VALUES (:message)"); $this->connection->beginTransaction(); foreach ($eventStream as $event) { if (!$stmt->execute(['message' => $this->eventSerializer->serialize($event)])) { $this->connection->rollBack(); } } $this->connection->commit(); } public function load(AggregateId $id) { $stmt = $this->connection->prepare("SELECT data FROM event_store WHERE data->'payload'->>'aggregate_id' = :id"); $stmt->execute(['id' => (string) $id]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $events = []; foreach ($rows as $row) { $events[] = $this->eventSerializer->deserialize($row['data']); } return new EventStream($events); } }
  33. 33. When CQRS meets Event Sourcing / Event sourcing
  34. 34. namespace UlaboxChangoInfrastructurePersistenceModelReception; class EventSourcingReceptionRepository implements ReceptionRepository { public function save(Reception $reception) { $events = $reception->recordedEvents(); $this->eventStore->append($reception->id(), $events); foreach ($events as $event) { $this->eventBus->dispatch($event); } } public function load(ReceptionId $id) { $eventStream = $this->eventStore->load($id); return Reception::reconstituteFromEvents( new AggregateHistory($id, $eventStream) ); } }
  35. 35. namespace UlaboxChangoDomainModelReception; class Reception extends EventSourcedAggregate { public static function create( ReceptionId $id, DateTime $receptionDate, SupplierId $supplierId ) { $instance = new self($id); $instance->recordThat( new ReceptionWasScheduled($id, $receptionDate, $supplierId) ); return $instance; } protected function applyReceptionWasScheduled(ReceptionWasScheduled $event) { $this->receptionDate = $event->receptionDate(); $this->supplierId = $event->supplierId(); $this->status = ReceptionStatus::PENDING(); $this->containers = new ArrayCollection(); } }
  36. 36. namespace UlaboxChangoDomainModel; abstract class EventSourcedAggregate implements AggregateRoot, EventRecorder { protected function __construct() { $this->version = 0; $this->eventStream = new EventStream(); } protected function recordThat(Event $event) { $this->apply($event); $this->eventStream->append($event); } protected function apply(Event $event) { $classParts = explode('', get_class($event)); $methodName = 'apply'.end($classParts); if (method_exists($this, $methodName)) { $this->$methodName($event); } $this->version++; } }
  37. 37. namespace UlaboxChangoDomainModelReception; class Reception extends EventSourcedAggregate { public static function reconstituteFromEvents(AggregateHistory $history) { $instance = new self($history->aggregateId()); foreach ($history->events() as $event) { $instance->apply($event); } return $instance; } public function addContainer(Temperature $temperature, array $containerLines) { $containerId = ContainerId::create( $this->id(), $temperature, count($this->containers) ); $this->recordThat( new ContainerWasAdded($this->id, $containerId, $temperature) ); foreach ($containerLines as $line) { $this->addLine( $containerId, $line->label(), $line->quantity(), $line->type() ); } } protected function applyContainerWasAdded(ContainerWasAdded $event) { $container = new Container($event->containerId(), $event->temperature()); $this->containers->set((string) $event->containerId(), $container); } }
  38. 38. Conclusions
  39. 39. When CQRS meets Event Sourcing / Conclusions Benefits ● Decoupling ● Performance in Read Model ● Scalability ● No joins ● Async with internal events and consumers ● Communicate other bounded contexts with events
  40. 40. When CQRS meets Event Sourcing / Conclusions Problems found ● With DDD ○ Decide aggregates => talk a LOT with the domain experts ○ Boilerplate => generate as much boilerplate as possible ● With CQRS ○ Forgetting listeners in read model ○ Repeated code structure ● With event sourcing ○ Adapting your mindset ○ Forgetting applying the event to the entity ○ Retro compatibility with old events ● Concurrency/eventual consistency
  41. 41. Work with us!
  42. 42. When CQRS meets Event Sourcing / Work with us Work with us
  43. 43. Thanks to...
  44. 44. When CQRS meets Event Sourcing / Conclusions
  45. 45. Thank you! Questions? @manelselles manelselles@gmail.com

    Be the first to comment

    Login to see the comments

  • edysanchez

    Jun. 23, 2016
  • OriolGonzlezManzano

    Jun. 23, 2016
  • powerirs

    Jul. 18, 2016
  • mewmf

    Jul. 27, 2016
  • txetxuda

    Sep. 17, 2016
  • VladimirBloshchitsyn

    Sep. 22, 2016
  • juntaoq

    Sep. 30, 2016
  • liopic

    Dec. 15, 2016
  • ebarbeito

    Jan. 8, 2017
  • StrasserMathias1

    Mar. 9, 2017
  • joobn

    Oct. 28, 2017
  • xfornesa

    Jan. 28, 2018
  • AlexKulina

    Jul. 27, 2018
  • BeatEspiaDiaz

    Mar. 8, 2019
  • MikoajCzajkowski1

    Oct. 14, 2019
  • jigen79

    Apr. 16, 2020

Have you ever wondered how all this CQRS / ES concepts could be applied to a PHP project? Let's take a look at some code of our last project: a warehouse management system. This talk will show how we understand DDD and how we apply it at Ulabox, what changes in application's architecture and code when we apply CQRS and how to deal with event sourcing, when there're no persisted entities, only events that generate projections used in the read model. Speaker: Manel Sellés (@manelselles), software engineer, DDD-TDD fan and Symfony Expert Certified by Sensiolabs. Currently developing enterprise logistics software at Ulabox.com

Views

Total views

4,077

On Slideshare

0

From embeds

0

Number of embeds

479

Actions

Downloads

55

Shares

0

Comments

0

Likes

16

×