CQRS AND EVENT SOURCING BASICS
Alexander Miertsch
Founder and CEO of prooph software GmbH
Founder and core contributor prooph components
contact@prooph.de
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
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,727