- Order placed, then payment, inventory and shipping react
- Sending notifications on business events
- Adding loyalty points or analytics as new consumers
In a ballet, no conductor tells each dancer when to move; every dancer knows the choreography and reacts to the music and to each other. In Choreography, services work the same way: there’s no central coordinator. Each service listens for events, does its part, and publishes new events that others react to. The overall business process emerges from these reactions.
It’s the most loosely coupled way to coordinate services — and, past a certain complexity, the hardest to understand.
The problem
A business process spans several services: placing an order involves payment, inventory, shipping, and notifications. Something must make sure each step happens. You can:
- have one service call the others in sequence (tight temporal coupling, cascading failures),
- introduce an orchestrator that commands each step (Orchestration),
- or let services react to each other’s events — choreography.
How it works
sequenceDiagram
participant O as Order
participant B as Event bus
participant P as Payment
participant I as Inventory
participant S as Shipping
participant N as Notifications
O->>B: OrderPlaced
B->>P: OrderPlaced
B->>N: OrderPlaced (send "we got your order")
P->>B: PaymentCaptured
B->>I: PaymentCaptured
I->>B: StockReserved
B->>S: StockReserved
S->>B: ShipmentScheduled
B->>O: ShipmentScheduled (order → CONFIRMED)
B->>N: ShipmentScheduled (send tracking email)
Each service:
- owns its step and its data,
- subscribes to the events that trigger its work,
- publishes events describing what it did (Domain Events),
- knows nothing about who consumes its events.
Adding a new reaction — say, a loyalty service that awards points on ShipmentScheduled — requires no change to existing services.
Failure handling: compensating by events
When a step fails, the failing service publishes a failure event, and earlier participants react with compensations. This is a choreographed Saga.
flowchart TD
A([OrderPlaced]) --> P[Payment: capture]
P -- PaymentCaptured --> I[Inventory: reserve]
P -- PaymentDeclined --> O1[Order: mark REJECTED]
I -- StockReserved --> S[Shipping: schedule]
I -- StockUnavailable --> R[Payment: refund]
R -- PaymentRefunded --> O2[Order: mark CANCELLED]
S -- ShipmentScheduled --> O3[Order: mark CONFIRMED]
Example: a choreographed participant (Kafka, Java)
@Component
public class InventoryParticipant {
private final InventoryService inventory;
private final OutboxPublisher outbox; // reliable publishing in the same transaction
@KafkaListener(topics = "payments.events", groupId = "inventory")
@Transactional
public void on(PaymentEvent event, @Header("message-id") String messageId) {
if (!(event instanceof PaymentCaptured captured)) return; // only react to what we care about
if (inbox.alreadyProcessed(messageId)) return; // idempotent consumer
ReservationResult result = inventory.reserve(captured.orderId(), captured.lines());
switch (result) {
case Reserved r -> outbox.publish("inventory.events",
new StockReserved(captured.orderId(), r.reservationId(), captured.correlationId()));
case Unavailable u -> outbox.publish("inventory.events",
new StockUnavailable(captured.orderId(), u.missingSkus(), captured.correlationId()));
}
inbox.markProcessed(messageId);
}
}
Three building blocks appear in every reliable choreography:
- Transactional Outbox — publishing events atomically with state changes.
- Idempotent Consumer — tolerating redelivered events.
- Correlation ID — tying all events of one business process together.
Making the invisible flow visible
Choreography’s main weakness: nobody owns the end-to-end flow. It’s spread across subscriptions in many codebases. Mitigations:
- Event catalog / AsyncAPI — document which service publishes and consumes which events.
- Distributed tracing — propagate trace context through message headers so one order’s journey shows as one trace. See Distributed Tracing.
- Process monitoring — a passive “observer” that consumes all events for a process and tracks its state (and alerts on stuck processes) without controlling it.
- Timeouts — someone must notice when
StockReservednever arrives. Usually the service that started the process (Order) schedules a timeout. - Event storming — model the flow visually with domain experts before implementing it.
Choreography vs. orchestration
| Choreography | Orchestration | |
|---|---|---|
| Coordination | Distributed, via events | Central coordinator sends commands |
| Coupling | Services know events, not each other | Orchestrator knows every participant |
| Adding a reaction | Subscribe — no other changes | Change the orchestrator |
| Visibility of the flow | Implicit; needs tooling | Explicit; one place to read |
| Complex branching, timeouts, human steps | Awkward | Natural |
| Single point of failure | None | The orchestrator (HA mitigates) |
| Risk | Cyclic dependencies, “event spaghetti” | God-orchestrator with business logic |
Rule of thumb: choreograph simple, linear reactions and fan-out notifications (two to four steps, few branches). Orchestrate long, branching, time-bound processes where someone needs to answer “what state is this in?”. Many systems use both: orchestration within a bounded context, choreography between contexts.
Trade-offs
| Benefits | Costs |
|---|---|
| Very loose coupling between services | End-to-end flow is hard to see and change |
| No central bottleneck or single point of failure | Debugging spans many services and logs |
| Easy to add new consumers | Risk of cyclic event chains |
| Services remain autonomous and independently deployable | Timeouts and stuck processes need explicit design |
Common pitfalls
Warning: event chains that loop. Service A reacts to B’s event and emits one that B reacts to, which A reacts to… Usually unintended, sometimes infinite. Map your event flows and watch for cycles.
- Commands disguised as events (
ReserveStockRequested) — that’s orchestration by another name, with less clarity. - Consumers depending on event order across topics — ordering is only guaranteed within a partition.
- No owner for the process — every team owns a step, nobody owns the outcome.
- Changing an event’s meaning and silently breaking unknown consumers.
Tools and real-world usage
- Apache Kafka, RabbitMQ topic exchanges, AWS EventBridge / SNS+SQS, Google Pub/Sub, Azure Service Bus topics and Event Grid.
- AsyncAPI, EventCatalog — documentation for event-driven systems.
- OpenTelemetry messaging semantic conventions — tracing across asynchronous hops.
Related patterns
- Orchestration — the centralized alternative.
- Publish-Subscribe — the messaging mechanism underneath.
- Saga — choreographed sagas for consistency.
- Domain Events — the vocabulary of choreography.
Key takeaways
- Services react to events and publish their own; no one is in charge.
- It maximizes decoupling and makes adding consumers trivial.
- Use outbox, idempotent consumers, and correlation IDs as standard equipment.
- Invest in visibility — tracing, event catalogs, process monitors — and switch to orchestration when flows get complex.