Event-Driven Architecture: Choreography vs Orchestration
Going event-driven decouples your services — and immediately forces a choice about how a multi-step workflow is coordinated. Choreography or orchestration? Here's the trade-off and how I decide.
The moment you break a monolith into services, one question shows up: how do services that no longer share a database coordinate? Event-driven architecture answers it by having services announce facts — "an order was placed" — instead of calling each other directly. Producers don't know who's listening. Consumers react on their own.
That decoupling is the whole appeal. But it immediately splits into two very different styles of coordinating a workflow, and picking the wrong one is a decision you'll regret for a long time.
Events, not commands
First, the mental shift. A command tells a specific service to do something: chargeCard(order). An event states that something happened: OrderPlaced. The publisher of an event doesn't care who reacts — payment, inventory, and email can all subscribe, or none can, and the publisher's code doesn't change.
That's what buys you decoupling: you add a new consumer to a stream without touching the producer at all.
Choreography: everyone reacts on their own
In choreography, there's no central coordinator. Each service listens for events and emits its own. The workflow emerges from the chain of reactions.
OrderPlaced
├→ Payment service → emits PaymentCompleted
├→ Inventory service → emits StockReserved
PaymentCompleted + StockReserved
└→ Shipping service → emits OrderShippedNobody "runs" this flow — it happens because each service knows its own job. The upside is maximum decoupling and easy extension: a new loyalty-points service just subscribes to OrderPlaced and nobody else knows or cares.
The downside shows up when something goes wrong. There's no single place that knows the state of an order. To answer "why is this order stuck?" you're stitching together logs from five services. And the business logic of the workflow is smeared across all of them — no one file tells you the sequence.
Orchestration: a conductor runs the flow
In orchestration, one component — an orchestrator, often a saga — explicitly drives the steps. It sends commands and waits for replies, holding the workflow's state itself.
Orchestrator:
1. command: ChargePayment → wait for reply
2. command: ReserveStock → wait for reply
3. command: ShipOrder → wait for reply
(on any failure → run compensating actions in reverse)Now the workflow lives in one place. You can read it, you can see exactly where an order is, and — crucially — you can define what happens when step 2 fails after step 1 succeeded. That last part is the saga pattern: since you can't have a distributed transaction, you run compensating actions to undo completed steps (refund the payment because the stock reservation failed).
How I choose
My rule of thumb, from building both:
- Choreography for loosely-related reactions — notifications, analytics, cache invalidation, anything where "publish a fact and let subscribers do their thing" is the whole story. Simple, decoupled, easy to extend.
- Orchestration for genuine multi-step business transactions that must complete or roll back as a unit — checkout, onboarding, provisioning. When correctness across steps matters and failure needs a defined recovery, you want one place that owns the sequence.
Plenty of real systems use both: orchestration for the critical transactional core, choreography for the fan-out of side effects around it.
The cost you're always paying
Whichever you pick, event-driven systems trade the simplicity of a synchronous call for the resilience of decoupling — and the currency is eventual consistency. There's a window where the order exists but payment hasn't processed. Your data model, your UI, and your users have to tolerate that gap. If they genuinely can't, that part of the system might just want to stay a plain synchronous call. Decoupling is powerful, but it isn't free, and pretending the gap doesn't exist is how event-driven systems earn their reputation for being hard to debug.