Read time: ~

Event-Driven Microservices with RabbitMQ

Events vs commands, choreography vs orchestration, and designing and versioning events across Order, Inventory, and Notification services.

Prerequisite:Core Messaging Patterns and the Reliability section. You’ll need: The Order, Inventory, and Notification services from earlier modules.

You now have every building block: routing, serialization, acks, confirms, dead-lettering, and idempotency. This module steps back to architecture: how to compose services into an event-driven system that stays loosely coupled as it grows.


What you’ll be able to do after this module

  • Distinguish an event from a command and choose the right one.
  • Compare choreography and orchestration and pick per situation.
  • Design event payloads that survive change.
  • Version events without breaking existing consumers.

1. Events vs commands

Both travel as RabbitMQ messages, but they express different intent:

 CommandEvent
Intent“Do this”“This happened”
NamingImperative: ReserveStockPast tense: OrderCreated
Knows the recipientYes, one handlerNo, any number of subscribers
CouplingHigherLower

Events are the backbone of loose coupling. The Order Service announces OrderCreated without knowing or caring who reacts. Commands still have their place for directed work, but prefer events for cross-service reactions.


2. The scenario as an event-driven system

The Order Service publishes one OrderCreated event. Inventory and Notification each subscribe with their own queue, so both react independently.

flowchart TD
    Client["Client"]
    Order["Order Service"]
    EXorders.topic
    QI["inventory.queue"]
    QN["notification.queue"]
    Inv["Inventory Service"]
    Notif["Notification Service"]
    Client -->|"POST /orders"| Order
    Order -->|"publish order.created"| EX
    EX -->|"order.created"| QI
    EX -->|"order.created"| QN
    QI --> Inv
    QN --> Notif
    Inv -->|"publish stock.reserved"| EX
    EX -.->|"stock.reserved"| QN

Adding a fourth reaction (fraud checks, analytics) is a new service with its own queue bound to the exchange. The Order Service never changes.


3. Choreography vs orchestration

Two ways to coordinate a multi-step business process.

Choreography

Each service reacts to events and emits its own. There is no central coordinator; the flow emerges from the chain of events.

flowchart LR
    O["Order"] -->|OrderCreated| I["Inventory"]
    I -->|StockReserved| N["Notification"]
    I -->|StockReserved| S["Shipping"]
  • Strengths: maximum decoupling, easy to add reactions.
  • Weaknesses: the end-to-end flow is implicit and harder to trace across services.

Orchestration

A coordinator service issues commands and tracks progress.

flowchart TD
    Orch["Order Orchestrator"]
    Orch -->|ReserveStock| I["Inventory"]
    Orch -->|ChargePayment| P["Payment"]
    Orch -->|SendConfirmation| N["Notification"]
    I -.->|reply| Orch
    P -.->|reply| Orch
  • Strengths: the flow lives in one place and is explicit.
  • Weaknesses: the orchestrator is a coupling point and can become a bottleneck.

Use choreography for simple fan-out reactions and orchestration when a process has many ordered steps with compensation. The saga patterns in the next module apply to both.


4. Designing events that last

An event is a contract other teams depend on. Design for change:

  • Include a stable identity and a timestamp. An event id (for idempotency) and an occurredAt help consumers dedup and order.
  • Carry enough context, not the whole world. Include what consumers realistically need so they need not call back synchronously, but avoid dumping internal state.
  • Prefer additive change. Adding an optional field is safe; renaming or removing one is not.
  • Name in the past tense and keep the routing key stable (order.created).
public record OrderCreated(
        String eventId,        // for idempotency
        String orderId,
        String sku,
        int qty,
        Instant occurredAt,
        int schemaVersion) {}

5. Schema versioning

Consumers and producers deploy independently, so an event’s shape will change while old consumers still run. Strategies, from simplest:

  • Tolerant reader: consumers ignore unknown fields and tolerate missing optional ones. Configure the JSON converter to not fail on unknown properties. This handles most additive changes with no coordination.
  • Version field: carry schemaVersion in the payload so a consumer can branch when semantics change.
  • New routing key or exchange for breaking changes: publish order.created.v2 alongside order.created, letting old and new consumers coexist until the old one is retired.

Caution: Never make a breaking change to an event in place. Add a new version and migrate consumers, then retire the old one. Recall the trusted-packages type mapping from First Producer and Consumer; versioning and mapping together keep producers and consumers decoupled.


Checkpoint

You should now be able to:

  • Choose between an event and a command for an interaction.
  • Decide between choreography and orchestration.
  • Design an event payload with identity, context, and a version.
  • Evolve an event schema without breaking existing consumers.