DEV Community

Swetha Golla
Swetha Golla

Posted on • Originally published at swethagolla-eng.github.io

Choreography vs Orchestration: The Real Trade-off in Event-Driven Systems

By Swetha Golla ยท ~6 min read ยท Senior Application Architect

๐Ÿ”— This post has a live interactive version with a clickable choreography-vs-orchestration trace comparison and a wiring diagram of the same order flow shown both ways: read it here

TL;DR

  • Choreography: services publish events and react to what they hear. No central brain, great decoupling, but the workflow only exists as the sum of every service's subscriptions โ€” nobody can point to the code and say "this is the order flow."
  • Orchestration: one coordinator calls each service and owns the sequence. Easy to trace, easy to test, but that coordinator becomes a dependency every step is coupled to.
  • For payments and anything an auditor will ask about, orchestration usually wins โ€” a single, queryable saga state beats stitching together five services' logs by correlation ID.
  • For high fan-out, many-teams systems where eventual consistency is genuinely fine, choreography wins โ€” but only if you invest in distributed tracing before you need it, not after.

Two ways to wire the same three services

Take the most common saga in commerce: an order comes in, inventory gets reserved, a card gets charged, shipping gets notified. Chris Richardson, who did more than anyone to formalize the microservices Saga pattern (the underlying idea traces back to a 1987 database paper by Garcia-Molina and Salem), defines the two coordination styles precisely: choreography is when "each local transaction publishes domain events that trigger local transactions in other services," and orchestration is when "an orchestrator... tells the participants what local transactions to execute." That's the whole distinction โ€” who decides what happens next, and how that decision gets communicated.

Martin Fowler's note on event-driven systems is worth reading before you pick either one, because it separates a question people conflate: are you using events to notify another service that something happened (loosely coupled, "figure out what to do with this"), or as a disguised command ("do this thing, I just don't want to call it a command")? Choreography only stays healthy when every event is a genuine notification. The moment a service publishes PaymentDeclined expecting exactly one downstream service to release inventory, you've built an orchestration and pretended it's a choreography โ€” with none of orchestration's visibility.

Same flow, two traces

This is the same "order placed โ†’ inventory reserved โ†’ payment charged โ†’ shipping notified" flow, wired two ways โ€” and it's exactly what the runnable Python demo in the companion repo prints.

Choreography โ€” events, no central caller

Four services, four independent subscriptions. The Order Service never calls Inventory; it just publishes OrderPlaced and moves on. Whoever's listening reacts.

EVENT   OrderPlaced          {order_id: ORD-1001, ...}
  -> InventoryService reacts to OrderPlaced
EVENT   InventoryReserved    {order_id: ORD-1001, ...}
  -> PaymentService reacts to InventoryReserved
EVENT   PaymentCharged       {order_id: ORD-1001, ...}
  -> ShippingService reacts to PaymentCharged
EVENT   ShippingNotified     {order_id: ORD-1001, ...}
Enter fullscreen mode Exit fullscreen mode

โš  Watch for: no line of code anywhere says "the flow is inventory then payment then shipping" โ€” that sequence only exists as the sum of the subscriptions above.

Orchestration โ€” one coordinator, direct calls

One function owns the recipe. It calls each service, checks the result, and decides what happens next โ€” including the compensating action when a step fails.

CALL    Orchestrator.start(order=ORD-1001)
  -> CALL Orchestrator -> InventoryService.reserve()
  <- OK   InventoryService: reserved
  -> CALL Orchestrator -> PaymentService.charge()
  <- OK   PaymentService: charged
  -> CALL Orchestrator -> ShippingService.notify()
  <- OK   ShippingService: notified
STATE   order ORD-1001 = COMPLETE
Enter fullscreen mode Exit fullscreen mode

โš  Watch for: every participant now depends on the orchestrator being up and its contract staying stable โ€” that's the coupling you're trading for the readability above.

Failure handling is where the two styles diverge hardest, and it's the part diagrams tend to hide. In choreography, a compensating action โ€” say, releasing an inventory hold after a payment decline โ€” has to be triggered by yet another event, published by whichever service happens to be listening for the failure. That service now has an implicit dependency on a failure mode it didn't cause, and that dependency lives in a subscription, not in a place a new engineer would think to look. In orchestration, the same compensation is the next line in the same function that made the failing call โ€” the "what do we undo" logic sits right next to the "what did we try" logic, because one person wrote both on purpose.

The trade-off that actually matters

The marketing version of this comparison โ€” "choreography is decoupled and scalable, orchestration is simple and traceable" โ€” is true but incomplete. The sharper way to think about it: choreography trades a debugging tax now for an organizational scaling benefit later. Orchestration trades a coupling cost now for an audit trail you get for free.

In an order-processing system, that second trade usually wins. When a chargeback dispute lands, or a regulator asks "walk me through what happened to this specific transaction," you want one place to look โ€” the orchestrator's saga state โ€” not a promise that your distributed tracing was configured correctly on the day it mattered. AWS's own writeup of this exact problem is blunt about it: choreography "obfuscates the workflow definition... there is no formal statement that describes steps, permitted transitions, and possible failures." That's not a knock on choreography as a pattern โ€” it's a direct statement of what you're giving up, from the vendor whose entire business is selling you the event bus.

Where choreography genuinely wins is fan-out and team autonomy at scale: one OrderPlaced event triggering inventory, fraud scoring, loyalty points, and analytics โ€” four teams, four independent deployments, none of them waiting on a shared orchestrator's release calendar. If you have five or fewer services in the saga and one team that owns the whole business outcome, you don't have that problem yet, and orchestration's traceability is close to free. If you have real fan-out across team boundaries, choreography's decoupling is worth the tracing investment โ€” but budget for it as a first-class requirement, not a "we'll add correlation IDs later" line item, because "later" is always after the first multi-hour incident where nobody can reconstruct what happened.

Dimension Choreography Orchestration
Where the flow lives Nowhere explicitly โ€” it's the sum of every service's event subscriptions. One place โ€” the orchestrator's code or state machine definition.
Debugging "why did this fail" Cross-service log correlation, usually by a trace/order ID, across N services. One saga state to read โ€” the failure step and compensation are adjacent.
Coupling Low โ€” services depend on event contracts, not on each other directly. Higher โ€” every participant is coupled to the orchestrator's availability and contract.
Best fit High fan-out, many independent teams, eventual consistency is acceptable. Few services, one team or tight coordination, auditability matters (compliance, payments, regulated flows).

Rule of thumb: Fewer than ~5 services in the saga, one team accountable for the outcome, or an auditor/regulator will ask "what happened to this transaction" โ€” orchestrate. Real fan-out across independent teams and eventual consistency is genuinely fine โ€” choreograph, but treat correlation IDs and distributed tracing as day-one infrastructure, not a retrofit.

The part people underestimate

Most teams don't consciously choose choreography โ€” they back into it because an event bus is already there and adding one more subscriber feels free. It is free, right up until someone asks for a sequence diagram of what actually happens when an order is placed, and the honest answer is "check five repos." Orchestration isn't free either โ€” a Step Functions state machine or a Temporal workflow is a real piece of infrastructure with its own on-call rotation. But it's infrastructure you're choosing on purpose, with a name and an owner, instead of a workflow you discover after the fact by reading Kafka topics.

See the working example โ†’ A runnable choreography-vs-orchestration trace comparison. Which pattern have you shipped in production โ€” did the trade-off play out the way the table above suggests? I'd genuinely like to hear where it didn't.

Sources & Further Reading

Top comments (0)