CQRS AND EVENT SOURCING BASICS

Alexander Miertsch

Founder and CEO of prooph software GmbH

Founder and core contributor prooph components

 

 

contact@prooph.de

@codeliner

@prooph_software

prooph components

CQRS and Event Sourcing packages for PHP

Supported Web Frameworks

or any other framework/codebase supporting container-interop & interop-config

prooph team

Community

Join prooph

Agenda

 CQRS and Event Sourcing Basics

time to grab a beer

Event Sourcing case study

Networking

Problem Space

Complex

Distributed

Chaotic

When to use CQRS & ES ?

Domain Events

to the rescue

 

Informational message

Communicate facts that have happened

Relevant to the business

 

Event Storming

Deeper insights with events

Event Flow

record, publish, react

OrderPlaced

OrderPayed

OrderShipped

Change State, Then Publish

ORM - Publisher

<?php

final class OrderService
{
    private $entityManager;
    private $orderRepositoy;
    private $eventPublisher;

    public function placeOrder(string $orderId, OrderItem ...$items): void
    {
        $order = $this->orderRepostoy->get($orderId);
        $this->entityManager->beginTransaction();
        
        try {
            $order->place($items);
            $this->entityManager->commit();
        } catch(\Throwable $error) {
            $this->entityManager->rollBack();
            throw $error;
        }
        
        $event = OrderPlaced::fromOrder($order);
        $this->eventPublisher->publish($event);        
    }
}

Record Event, Then Publish

Event Store - Publisher

<?php

final class PlaceOrderHandler
{
    private $eventStore;
    private $eventPublisher;

    public function handle(PlaceOrder $command): void
    {
        //Note: Loading/saving is normally done by event sourcing repository
        $orderStream = new StreamName('order-'.$command->orderId());
        $history = $this->eventStore->load($orderStream);
        
        $order = Order::reconstituteFromHistory($history);
        $newEvents = $order->place($command->items());
        
        //Atomic operation
        $this->eventStore->appendTo($orderStream, $newEvents);
                
        $this->eventPublisher->publish($newEvents);        
    }
}

Event Sourcing

State is a left fold of past events

# E1 at t1: UserRegistered
userId: 1
name: John Doe
email: doe@fake.com

# E2 at t2: UsernameChanged
userId: 1
oldName: John Doe
newName: Alex

# E3 at t3: EmailChanged
userId: 1
oldEmail: doe@fake.com
newEmail: contact@prooph.de

_______________________________________________

# current state (t:now) = ((E1 + E2) + E3):
userId: 1
name: Alex
email: contact@prooph.de
version: 3

Time Travel

Debug like a boss

UserRegistered

UsernameChanged

ChangeEmailFailed

{

E1 + E2

Change Email

Temporal Queries

Use time interval as query param

UserRegistered

UsernameChanged

5 min

7 min

Who has changed username within 5 min after registration?

UserRegistered

UsernameChanged

Event Sourcing in PHP

prooph/

event-sourcing

Event sourcing basics

Event store integration

 

prooph/

event-store

Append-only streams

Persistent projections

Event stream queries

CQRS

SRP on system level

Gateway

Write Model

Read Model

CQRS and Event Sourcing

Double Team

Gateway

Write Model

Read Model

Event Store

Projection

Cache

Break

Questions?
Time to grab a beer!

Agenda

 CQRS and Event Sourcing Basics

time to grab a beer

Event Sourcing case study

Networking

The Problem Space

Simplified version of a true story

 

The Problem Space

Bob runs a successful fashion business

New Strategy: Simplify and modernize the sales process

The Problem Space

Bettina is one of the many account managers in Bob's company

The Problem Space

Petra is a shop owner and one of 50.000 business customers of Bob

Bettina is one of the many account managers in Bob's company

The Problem Space

John is part of the customer service team in Bob's company

Petra is a shop owner and one of 50.000 business customers of Bob

Bettina is one of the many account managers in Bob's company

The Problem Space

Strategy: Simplify and modernize the sales process

requires negotiation of new contracts

The Problem Space

Contract App

The Contract App

  • Supports account manager during contract negotiation
  • Provides customer search and filter capabilities
  • Dynamic multistep data form
  • In-browser/realtime validation of complex decision rules
  • Offline functionality

Symfony + prooph = 

EAI

Bettina needs to find Petra out of 50.000 customers in the Contract App

EAI with Event Sourcing

Customer data is in a legacy system

Some Customer Events ...

Customer Import Stream

Some Customer Events ...

Some Customer Events ...

EAI with Event Sourcing

Customer data is in a legacy system

Some Customer Events ...

Customer Import Stream

Petra's shop was added

Some Customer Events ...

Petra's shop was updated

Nightly projection into customer search

Nightly XML/CSV export

Some Customer Events ...

Customer Projections

  • No heavy ORM required, customer data is read only
  • Read model acts as a cache

Focus on Behavior

Contract Stream

Appointment was made

Some Contract Events ...

Special conditions were requested

Some Contract Events ...

Send mail to John

Focus on Behavior

Contract Stream

Special conditions were requested

Some Contract Events ...

Special conditions were accepted

Some Contract Events ...

Send to external signing service

Monitor External Service

Contract Stream

Contract signing was started

Some Contract Events ...

Contract was internally signed

Some Contract Events ...

Contract was externally signed

Event Sourcing

Event Sourcing for enterprise application integration (EAI)

Manage business processes with event sourcing

Monitor long-running business processes

A Case Study

Save The Date

Thanks for listening

Questions?

File Imports

//Symfony\Console command, reads CSV file -> row => domain command
private function processDeskwareDataCollection(OutputInterface $output): void
{
    //Use of ProgressBar omitted
    $failed = 0;
    foreach ($this->tmpDeskwareDataCollection as $deskwareData) {
        try {
            $domainCommand = new ImportCustomerDeskwareData($deskwareData->toArray());
            $this->commandBus->dispatch($domainCommand);
        } catch (CommandDispatchException $ex) {
            //print and log error for row ...
            $failed++;
        }
    }

    if ($failed > 0) {
        $output->writeln(
            sprintf(
                '<error>%d entries failed. Please check the log for details</error>',
                $failed
            )
        );
        exit(1);
    }

    exit(0);
}

Service Bus for Symfony 

prooph_service_bus:
  command_buses:
    customer_command_bus:
      plugins:
        - 'prooph_service_bus.handle_command_invoke_strategy'
      router:
        type: 'prooph_service_bus.command_bus_router'
        routes:
          'ImportBasicCustomerData': 'customer_domain.import_basic_customer_data_handler'          
          'ImportDigitalInvoiceData': 'customer_domain.import_customer_digital_invoice_data_handler'          
          'ImportCustomerDeskwareData': 'customer_domain.import_customer_deskware_data_handler'
    contract_app_command_bus:
      # put command bus config for contract_app bundle here
  event_buses:
    contract_event_bus:
      plugins:
        - 'prooph_service_bus.on_event_invoke_strategy'
      router:
        # Service ID of the async message producer e.g. for Amazon AWS SQS
        async_switch: 'my_async_message_producer'
        type: 'prooph_service_bus.event_bus_router'
        routes:
          'ContractNegotiationWasCompleted':
            - 'contract_app.send_negotiation_completed_mail'
          'ContractNegotiationWasRejectedBySupervisor':
            - 'contract_app.send_negotiation_rejected_by_supervisor_mail'
            - 'contract_app.notify_customer_domain_about_rejected_negotiation'

Handle Commands




final class RequestSpecialConditionssHandler extends AbstractHandler
{
    public function handle(RequestSpecialConditions $command)
    {
        $acquisitionProcess = $this->getAcquisitionProcess($command->acquisitionProcessId());
        
        $acquisitionProcess->requestSpecialConditions(
            $command->accountManager(),
            $command->requestedSalesAreaDimension(),
            $command->requestedPlanedAnnualSales()
        );

        $this->acquisitionProcessRepository->save($acquisitionProcess);

        //SpecialConditionsRequested event is recorded internally 
        //and published on event bus after save
    }
}

Event Store for Symfony 



# prooph/event-store v6 compatible version
# prooph/event-store v7 bundle is wip
prooph_event_store:
  stores:
    contract_store:
      type: 'contract_app.event_store.mongo_db_adapter'
      repositories:
         # name of repository is registered as service id
         acquistion_process_repository:
            repository_class: 'Contract\Infrastructure\MongoD\AcquistionProcessRepository'
            aggregate_type: 'Contract\Model\AcquistionProcess'
            aggregate_translator: 'prooph_event_sourcing.aggregate_translator'
            stream_name: 'contract_stream'
            # snapshot_store: prooph_snapshot_store.mongo_db <-- not activated by default 
            # one_stream_per_aggregate: false <-- default

Sf UG Berlin Meetup CQRS and ES Basics

By prooph

Sf UG Berlin Meetup CQRS and ES Basics

  • 1,634