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:
| Command | Event | |
|---|---|---|
| Intent | “Do this” | “This happened” |
| Naming | Imperative: ReserveStock | Past tense: OrderCreated |
| Knows the recipient | Yes, one handler | No, any number of subscribers |
| Coupling | Higher | Lower |
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
occurredAthelp 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
schemaVersionin the payload so a consumer can branch when semantics change. - New routing key or exchange for breaking changes: publish
order.created.v2alongsideorder.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.