When cqrs meets event sourcing

45
When CQRS meets Event Sourcing A warehouse management system done in PHP

Transcript of When cqrs meets event sourcing

Page 1: When cqrs meets event sourcing

When CQRS meets Event SourcingA warehouse management system done in PHP

Page 2: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Ulabox

ULABOX

Page 3: When cqrs meets event sourcing

About me● @manelselles

● Backend at Ulabox

● Symfony Expert

Certified by Sensiolabs

● DDD-TDD fan

Page 4: When cqrs meets event sourcing

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

Page 5: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Warehouse

Page 6: When cqrs meets event sourcing

Please test!Good practices

Page 7: When cqrs meets event sourcing

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

Page 8: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Good practices

Continuous integration

Page 9: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Good practices

Other good practices● SOLID● Coding Style● Pair programming● Refactor

Page 10: When cqrs meets event sourcing

DDD-Hexagonal architecture

Page 11: When cqrs meets event sourcing

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

Page 12: When cqrs meets event sourcing

When CQRS meets Event Sourcing / DDD-Hexagonal

Aggregate

Page 13: When cqrs meets event sourcing

When CQRS meets Event Sourcing / DDD-Hexagonal

Hexagonal architecture

Page 14: When cqrs meets event sourcing

namespace Ulabox\Chango\Infrastructure\Ui\Http\Controller;

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 Ulabox\Chango\Infrastructure\Ui\Amqp\Consumer;

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

Page 15: When cqrs meets event sourcing

namespace Ulabox\Chango\Application\Service;

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

Page 16: When cqrs meets event sourcing

When CQRS meets Event Sourcing / DDD-Hexagonal

Why application service?● Same entry point● Coordinate tasks on model● Early checks● User authentication

Page 17: When cqrs meets event sourcing

namespace Ulabox\Chango\Domain\Model\Reception;

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

Page 18: When cqrs meets event sourcing

namespace Ulabox\Chango\Domain\Model\Reception\Container;

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

Page 19: When cqrs meets event sourcing

namespace Ulabox\Chango\Infrastructure\Persistence\Doctrine\Reception;

class DoctrineReceptionRepository implements ReceptionRepository{ public function get(ReceptionId $id) { return $this->find($id); }

public function save(Reception $reception) { $this->_em->persist($reception); }}

Page 20: When cqrs meets event sourcing

Let’s apply Command and Query Responsibility Segregation

Page 21: When cqrs meets event sourcing

When CQRS meets Event Sourcing / CQRS

CQRSSeparate:● 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

Page 22: When cqrs meets event sourcing

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

Page 23: When cqrs meets event sourcing

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

Page 24: When cqrs meets event sourcing

When CQRS meets Event Sourcing / CQRS

Page 25: When cqrs meets event sourcing

namespace Ulabox\Chango\Application\Service;

class ReceptionService{ public function addContainer(ReceptionId $receptionId, ContainerPayload $payload) { $command = new AddContainer($receptionId, $payload->temperature(), $payload->containerLines()); $this->commandBus->handle($command); }}

namespace Ulabox\Chango\Domain\Command\Reception;

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

Page 26: When cqrs meets event sourcing

namespace Ulabox\Chango\Domain\ReadModel\Reception;

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 Ulabox\Chango\Domain\ReadModel\Reception;

interface ReceptionView{ public function save(ReceptionProjection $reception);

public function receptionOfId(ReceptionId $receptionId);

public function find(Query $query);}

Page 27: When cqrs meets event sourcing

namespace Ulabox\Chango\Application\Service;

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

Page 28: When cqrs meets event sourcing

Let’s get crazy: event sourcing

Page 29: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Event sourcing

Event sourcing● Entities are reconstructed with events● No state● No database to update manually● No joins

Page 30: When cqrs meets event sourcing

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

Page 31: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Event sourcing

Event Store● PostgreSQL● jsonb● DBAL

Page 32: When cqrs meets event sourcing

namespace Ulabox\Chango\Infrastructure\Persistence\EventStore;

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

Page 33: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Event sourcing

Page 34: When cqrs meets event sourcing

namespace Ulabox\Chango\Infrastructure\Persistence\Model\Reception;

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

Page 35: When cqrs meets event sourcing

namespace Ulabox\Chango\Domain\Model\Reception;

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

Page 36: When cqrs meets event sourcing

namespace Ulabox\Chango\Domain\Model;

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

Page 37: When cqrs meets event sourcing

namespace Ulabox\Chango\Domain\Model\Reception;

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

Page 38: When cqrs meets event sourcing

Conclusions

Page 39: When cqrs meets event sourcing

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

Page 40: When cqrs meets event sourcing

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

Page 41: When cqrs meets event sourcing

Work with us!

Page 42: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Work with us

Work with us

Page 43: When cqrs meets event sourcing

Thanks to...

Page 44: When cqrs meets event sourcing

When CQRS meets Event Sourcing / Conclusions

Page 45: When cqrs meets event sourcing

Thank you!Questions?

@[email protected]