Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Event Streaming Architecture with Symfony Messe...

Event Streaming Architecture with Symfony Messenger

This talk explores event streaming architecture using the Symfony Messenger component. The talk will answer what event streaming is and when it's worth using. Then the talk dives into the code, showing how to design and implement event streaming with Messenger through practical examples.

Avatar for Max Beckers

Max Beckers

April 27, 2026

More Decks by Max Beckers

Other Decks in Technology

Transcript

  1. Max Beckers • Solutions Architect @ PAYONE • Building highly

    available systems in the payment industry • Working with Symfony since version 2 • Open-Source Contributor • Father of 2 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 2
  2. Understanding Event Streaming Through a Bank Account 23.04.2026 Event Streaming

    Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 3
  3. Bank Account Overview (State Oriented) Bank Account #12345678 Balance 7.883,12

    € But how did we get here? 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 4
  4. Bank Account Transaction History Balance 2025-12-31: 5.632,92 € Each row

    is an EVENT (describes what happened) Balance is the SUM of all events to that date Date Description Amount Balance 2026-01-01 Rent payment - 1.200,00 € 4.432,92 € 2026-01-05 Grocery Store - 45,30 € 4.387,62 € 2026-01-12 Coffee Shop - 4,50 € 4.383,12 € 2026-01-15 Salary + 3.520,00 € 7.903,12 € 2026-01-17 Toy Store - 20,00 € 7.883,12 € 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 5
  5. Key Principles Of Events Immutable Events can’t be changed or

    deleted, they happened Append Only New Events are added to the end Ordered Sequence matters, in most cases by date or sequence numbers Carry Meaning What happened with relevant data Past Tense MoneyDeposited, MoneyWithdrawn 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 6
  6. Events Are Immutable Facts 20,00 € was spent at the

    toy store. This is a fact. It happened and can’t un-happen. You don’t delete a transaction. You add a compensating event like a refund. Date Description Amount 2026-01-17 Toy Store - 20,00 € 2026-01-27 Toy Store REFUND + 20,00 € 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 7
  7. What Happens When Money Is Deposited? 23.04.2026 Event Streaming Architecture

    with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 8 MoneyDeposited AccountId: 12345678 Amount: 3,520.00€ Description: Salary Transaction History Update Account Balance Push Notification … MoneyWithdrawn AccountId: 12345678 Amount: 1,200.00€ Description: Rent Payment
  8. The Replay Problem “I just want to know how much

    money I have …” Bank Account opened: April 2011 Today: April 2026 15 Years with ~100 Tx/month ~18.000 events to replay to show the current balance Create a Snapshot and only replay since the last snapshot 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 9
  9. Handlers Must Be Idempotent Not Idempotent: 20€ 2x = 40€

    Idempotent: 20€ 2x = 20€ Processing the same event multiple times produces the same result as processing it once →Network failures can cause the same event to be delivered twice →Invalid balance in our bank account, email sent twice, … 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 10
  10. Why Event Streaming ? 23.04.2026 Event Streaming Architecture with Symfony

    Messenger Max Beckers - Symfony Live Berlin 2026 11 Decoupling Handlers don’t know about each other Scalability Async by default Resilience Consumer is down → Events wait Audit Trail Complete history of what happened Replayability Fix a bug → replay events Multiple Views Same event, different projections
  11. This was the theoretical part … 23.04.2026 Event Streaming Architecture

    with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 12
  12. … but how do I build this with Symfony ?

    23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 13
  13. Symfony Messenger Fundamentals Message Handler Transport Worker Message Bus Dispatch

    Sync Mode Get from 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 14
  14. Order Management System Example • Customer places an order in

    an online shop • Payment is processed at checkout • After a successful payment: • Customer receives a confirmation email • Invoice is generated and available in the customer portal • Internal stock management is updated • Webhook is sent to the configured subscribers 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 15
  15. The Flow 23.04.2026 Event Streaming Architecture with Symfony Messenger Max

    Beckers - Symfony Live Berlin 2026 16 Place Order Payment Produce Event Send Email Generate Invoice Update Stock Send Webhook … Events Topic
  16. Application Structure – Module Diagram 23.04.2026 Event Streaming Architecture with

    Symfony Messenger Max Beckers - Symfony Live Berlin 2026 17 API-Gateway Payment- Module Webhook- Module Invoice-Module Stock-Module Email-Module Order- Management
  17. Application Structure – Code src/ ├── ApiGateway/ │ ├── Controller/OrderController.php

    │ └── Internal/Converter/OrderConverter.php ├── OrderManagement/ │ ├── Dto/Order.php │ ├── Event │ │ ├── EventInterface.php │ │ └── OrderConfirmed.php │ ├── Interface/PlaceOrderHandler.php │ └── Internal/Handler/PlaceOrderHandlerImpl.php ├── Payment/[…] ├── Email/Internal/Handler/OrderEmailHandler.php ├── Invoice/Internal/Handler/InvoiceHandler.php ├── Stock/Internal/Handler/StockHandler.php └── Webhook/Internal/Handler/WebhookHandler.php 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 18 Order Management System Demo
  18. The Event <?php namespace App\OrderManagement\Event; final readonly class OrderConfirmed implements

    EventInterface { public function __construct( public string $eventId, public string $orderId, public string $customerId, public array $items, public float $totalAmount, public \DateTimeImmutable $occurredAt = new \DateTimeImmutable(), ) {} public static function getRoutingKey(): string { return 'order.confirmed'; } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 19 src/OrderManagement/Event/OrderConfirmed.php
  19. The PlaceOrderHandler <?php namespace App\OrderManagement\Internal\Handler; use … readonly class PlaceOrderHandlerImpl

    implements PlaceOrderHandler { public function __construct( private MessageBusInterface $bus, private PaymentProcessor $paymentProcessor ) {} public function placeOrder(Order $order): void { // store and handle payment $this->bus->dispatch(new OrderConfirmed( eventId: Uuid::v4()->toRfc4122(), orderId: $order->orderId, customerId: $order->customerId, items: $order->items, totalAmount: $order->totalAmount ), [new AmqpStamp(OrderConfirmed::getRoutingKey())]); } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 20 src/OrderManagement/Internal/Handler/PlaceOrderHandlerImpl.php
  20. The Email Handler <?php namespace App\Email\Internal\Handler; use App\OrderManagement\Event\OrderConfirmed; use Symfony\Component\Messenger\Attribute\AsMessageHandler;

    final class OrderEmailHandler { #[AsMessageHandler(fromTransport: 'email')] public function onOrderConfirmed(OrderConfirmed $event): void { // Send confirmation email to $event->customerId } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 21 src/Email/Internal/Handler/OrderEmailHandler.php
  21. The Invoice Handler <?php namespace App\Invoice\Internal\Handler; use App\OrderManagement\Event\OrderConfirmed; use Symfony\Component\Messenger\Attribute\AsMessageHandler;

    final class InvoiceHandler { #[AsMessageHandler(fromTransport: 'invoice')] public function onOrderConfirmed(OrderConfirmed $event): void { // Generate invoice PDF that the customer can download from a portal } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 22 src/Invoice/Internal/Handler/InvoiceHandler.php
  22. The Stock Handler <?php namespace App\Stock\Internal\Handler; use App\OrderManagement\Event\OrderConfirmed; use Symfony\Component\Messenger\Attribute\AsMessageHandler;

    final class StockHandler { #[AsMessageHandler(fromTransport: 'stock')] public function onOrderConfirmed(OrderConfirmed $event): void { foreach ($event->items as $item) { // Deduct stock for $item->productId by $item->quantity } } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 23 src/Stock/Internal/Handler/StockHandler.php
  23. The Webhook Handler <?php namespace App\Webhook\Internal\Handler; use App\OrderManagement\Event\OrderConfirmed; use Symfony\Component\Messenger\Attribute\AsMessageHandler;

    final class WebhookHandler { #[AsMessageHandler(fromTransport: 'webhook')] public function onOrderConfirmed(OrderConfirmed $event): void { // send a webhook to subscribers about order $event->orderId being confirmed. } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 24 src/Webhook/Internal/Handler/WebhookHandler.php
  24. Configuration 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers

    - Symfony Live Berlin 2026 25 config/packages/messenger.yaml framework: messenger: transports: # Publisher only - no queues declared, exchange is created by consumer transports events: dsn: '%env(RABBITMQ_DSN)%' options: auto_setup: false exchange: name: events type: topic # Each module owns its queue, all bound to same fanout exchange email: dsn: '%env(RABBITMQ_DSN)%' options: exchange: name: events type: topic queues: emails.queue: binding_keys: - 'order.confirmed' retry_strategy: … invoice: … stock: … webhook: … routing: 'App\OrderManagement\Event\EventInterface': events
  25. Test Request POST http://localhost:8000/orders Content-Type: application/json { "customerId": "customer-123", "items":

    [ {"id": "item-1", "name": "Widget A", "quantity": 2, "price": 4.99}, {"id": "item-2", "name": "Widget B", "quantity": 1, "price": 2.58} ], "totalAmount": 12.56 } ### HTTP/1.1 201 Created Content-Type: application/json Content-Length: 70 { "orderId": "o_268a7ae2-3388-4f15-b965-2283960b1e58", "status": "placed" } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 27 example/DummyOrder.rest
  26. Run Workers # One worker per module - fully independent

    php bin/console messenger:consume email php bin/console messenger:consume invoice php bin/console messenger:consume stock php bin/console messenger:consume webhook 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 28 CLI
  27. Email Worker 23.04.2026 Event Streaming Architecture with Symfony Messenger Max

    Beckers - Symfony Live Berlin 2026 29 Email worker receives OrderConfirmed from email.queue HandleMessageMiddleware Email OrderEmailHandler::onOrderConfirmed fromTransport: 'email' runs Invoice InvoiceHandler::onOrderConfirmed fromTransport: 'invoice' skipped Stock StockHandler::onOrderConfirmed fromTransport: 'stock' skipped Webhook WebhookHandler::onOrderConfirmed fromTransport: 'webhook' skipped
  28. Consumers With Supervisor [program:worker-email] command=php /home/www/bin/console messenger:consume email --time-limit=3600 user=ubuntu

    numprocs=2 startsecs=0 autostart=true autorestart=true startretries=10 [program:worker-invoice] command=php /home/www/bin/console messenger:consume invoice --time-limit=3600 ; … other configuration parameters [program:worker-stock] command=php /home/www/bin/console messenger:consume stock --time-limit=3600 ; … other configuration parameters [program:worker-webhook] command=php /home/www/bin/console messenger:consume webhook --time-limit=3600 ; … other configuration parameters 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 30 /etc/supervisor/conf.d/messenger-worker-order-management.conf
  29. Happy path working ☺ 23.04.2026 Event Streaming Architecture with Symfony

    Messenger Max Beckers - Symfony Live Berlin 2026 31
  30. Event Streaming Across Different Services 23.04.2026 Event Streaming Architecture with

    Symfony Messenger Max Beckers - Symfony Live Berlin 2026 32
  31. Modules To Deployed Services 23.04.2026 Event Streaming Architecture with Symfony

    Messenger Max Beckers - Symfony Live Berlin 2026 33 API-Gateway Payment- Module Webhook- Module Invoice-Module Stock-Module Email-Module Order- Management Email Service Stock Service Invoice Service
  32. Error Handling In Event Consumers 23.04.2026 Event Streaming Architecture with

    Symfony Messenger Max Beckers - Symfony Live Berlin 2026 34
  33. Retriable Errors Something went wrong, but might succeed later •

    Infrastructure Problems (DB down, network errors) • Bugs that are being fixed (deploy fix and retry) • Missing preconditions (dependent data not yet available) →Requeue message →Send to failure transport (DLQ) after max retries 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 35
  34. Bug In The Code 23.04.2026 Event Streaming Architecture with Symfony

    Messenger Max Beckers - Symfony Live Berlin 2026 36 Place Order Payment Produce Event Send Email Generate Invoice Update Stock Send Webhook … Division By Zero Events Topic
  35. Retry Handling • Configure Retry • Messages will automatically be

    retried e.g. 3 times • Configure failure transport • Messages are sent to failure transport failed_events • Fix the bug • Rerun the messages from the failure transport 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 37
  36. Run Workers # List all failed messages php bin/console messenger:failed:show

    -:transport=failed_events # Output: # ------ ---------------- --------------------- ------------------------------------ # Id Class Failed at Error # ------ ---------------- --------------------- ------------------------------------ # 1 OrderConfirmed 2026-04-15 14:23:11 RuntimeException: Division by zero # 2 OrderConfirmed 2026-04-15 14:25:33 RuntimeException: Division by zero # ------ ---------------- --------------------- ------------------------------------ # Retry all failed messages php bin/console messenger:failed:retry -:transport=failed_events 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 38 CLI
  37. Non-Retriable Errors Retry will never help, handle explicitly • Invalid

    data will always be invalid • Business rule violations 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 39
  38. Out Of Stock 23.04.2026 Event Streaming Architecture with Symfony Messenger

    Max Beckers - Symfony Live Berlin 2026 40 Place Order Payment Produce Event Send Email Generate Invoice Update Stock Send Webhook … OutOfStock Exception Events Topic
  39. And Now? • Email was already sent to the customer

    • Invoice was already generated • Webhook was sent →Publish a compensating event 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 41
  40. Handle The Compensating Event How to handle that ? Depends

    on the failure and the business! Refund (part of) the payment? Communicate with the customer and offer an alternative product? … 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 42
  41. The Compensating Event <?php namespace App\OrderManagement\Event; final readonly class StockUpdateFailed

    implements EventInterface { public function __construct( public string $eventId, public string $orderId, public string $customerId, public array $items, public float $totalAmount, public \DateTimeImmutable $occurredAt = new \DateTimeImmutable(), ) {} public static function getRoutingKey(): string { return 'stock.update_failed'; } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 43 src/OrderManagement/Event/StockUpdateFailed.php
  42. The Webhook Handler <?php namespace App\Webhook\Internal\Handler; use App\OrderManagement\Event\OrderConfirmed; use App\OrderManagement\Event\StockUpdateFailed;

    use Symfony\Component\Messenger\Attribute\AsMessageHandler; final class WebhookHandler { #[AsMessageHandler(fromTransport: 'webhook')] public function onOrderConfirmed(OrderConfirmed $event): void { // send a webhook to subscribers about order $event->orderId being confirmed. } #[AsMessageHandler(fromTransport: 'webhook')] public function onStockUpdateFailed(StockUpdateFailed $event): void { // POST to webhook subscribers } } 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 44 src/Webhook/Internal/Handler/WebhookHandler.php
  43. Takeaways • Events are immutable facts like bank statements •

    One event, many independent consumers • Each handler must be idempotent • Snapshots can prevent replay overhead • Monitor your failed queue • With Symfony Messenger, it is easy to implement Event Streaming 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 46
  44. When Should I (NOT) Use It? Use It When …

    • Event history is relevant • Multiple reactions on one event • Producers don't need to know their consumers • Independent Scaling • Processing delay is acceptable Don’t Use It When … • You need an immediate response • Request/Response would be simpler and sufficient • ACID transactions are required • The complexity outweighs the benefits 23.04.2026 Event Streaming Architecture with Symfony Messenger Max Beckers - Symfony Live Berlin 2026 47
  45. Thanks for listening ☺ 23.04.2026 Event Streaming Architecture with Symfony

    Messenger Max Beckers - Symfony Live Berlin 2026 48