System Design
How Orchestration and Choreography Work Together in Real Systems
Adding events to your architecture doesn't make it scalable - how you use them does. The secret isn't picking orchestration or choreography. It's knowing that the orchestrator itself emits events, and the two coordination styles interlock in one flow.
There's a myth worth killing early: simply using events in your architecture does not make it scalable. Sprinkling a message broker into your system and calling it "event-driven" guarantees nothing. What actually matters is how you use events - which flows are event-driven, how events are consumed, and, above all, who controls the coordination. The real skill isn't choosing orchestration or choreography; it's knowing that they're not mutually exclusive - the orchestrator itself can raise and listen to events, and in real systems the two styles interlock within a single flow. This post is about that interplay: a decision framework for who controls what, and how orchestration and choreography meet in practice.
(This builds on the dedicated orchestration-vs-choreography comparison - if you want the full trade-off breakdown and tooling, start there. Here we focus on the decision framework and how the two combine.)
The decision framework: who controls the flow?
Cut through everything, and the choice between orchestration and choreography comes down to a single question: who controls the flow?
- If the flow is critical and needs strict ordering, traceability, and error handling, it needs a central controller - that's orchestration. Someone must own the sequence and guarantee it happens correctly, in order, with clear failure handling.
- If the flow is non-critical and can tolerate loose coupling and independent reactions, it can be event-driven with no controller - that's choreography. No one needs to own the sequence; services just react.
Notice that this framework is driven by criticality, not by preference or by applying one style uniformly across the whole system. The decision should be intentional and per-flow - you look at each flow, ask how critical it is and whether it needs strict ordering, and let that determine who (if anyone) controls it. Applying orchestration everywhere makes a bottleneck; applying choreography everywhere loses control where you need it. The framework tells you which flows get which.
Critical flows → orchestration
For flows where failure has direct business impact and data integrity is non-negotiable, orchestration is the right choice. A central orchestrator service manages the sequence of steps - and crucially, while it can raise and listen to events, ultimately one single service owns the order of execution. That ownership is the point: someone is definitively in charge of what happens when.
The classic critical flows:
- Payment processing: charge → verify → confirm. Each step must happen in order, and a failure at any point needs precise handling.
- Refund workflows: validate → reverse payment → update order. Get the order wrong or skip a step, and you've mishandled someone's money.
- Order state updates: created → confirmed → shipped → delivered. The lifecycle must progress in strict sequence - you can't ship before confirming.
stateDiagram-v2
[*] --> Created
Created --> Confirmed
Confirmed --> Shipped
Shipped --> Delivered
Delivered --> [*]
note right of Confirmed: Orchestrated - one service<br/>owns this strict ordering
This state diagram shows an orchestrated order lifecycle: created, then confirmed, then shipped, then delivered, in strict order. Because the sequence must progress in exactly this order - you can't ship before confirming - a single orchestrator service owns and enforces the ordering. This strict, guaranteed sequencing is exactly what orchestration provides for critical flows.
These flows need guaranteed ordering, clear failure handling, and full traceability - and only a central orchestrator can provide those reliably. When you must be able to answer "exactly what happened to this payment, and in what order?", you need one service that owns and records the whole sequence.
Non-critical flows → choreography
For flows where independent reaction is sufficient and strict ordering isn't required, choreography is the better fit. Events are raised, and each service picks them up and reacts independently - no central controller dictating the sequence.
The classic non-critical flows:
- Notifications: order confirmed → send confirmation email or SMS. If the email is delayed by a few seconds, no harm done.
- Search index updates: product updated → re-index in the search engine (Elasticsearch). A moment's delay in the index is invisible to almost everyone.
- Analytics and recommendations: user purchased → update the recommendation engine. Eventually-processed is completely fine.
The tell is in the impact of delay or reordering: if a notification is a few seconds late, or a search-index update takes a moment longer, the business impact is minimal. These flows don't need a controller guaranteeing order - they need to scale independently and stay decoupled, which is exactly what choreography provides. Each service reacts on its own, and you add new reactions without touching anything central.
How the orchestrator interacts with events
Here's the nuance that most explanations miss, and it's the heart of how real systems actually work: orchestration doesn't mean you abandon events. The orchestrator itself can raise and listen to events. Orchestration and choreography aren't opposing camps - they compose.
Watch how they meet in one flow. The orchestrator runs the critical payment sequence (charge → verify → confirm) under tight control. Then, once payment is confirmed, the orchestrator emits an event - say, PaymentConfirmed. And from that moment, the flow switches modes: the notification service, the analytics service, and the search indexer all react to that event independently, via choreography. The orchestrator doesn't tell them what to do - it just announces "payment confirmed," and they each pick it up and do their own thing.
graph TD
subgraph ORCH["Orchestrated critical core (one owner, strict order)"]
O[Orchestrator] --> C[Charge] --> V[Verify] --> CF[Confirm]
end
CF --> EV{{"emits: PaymentConfirmed"}}
subgraph CHOR["Choreographed downstream (react independently)"]
EV --> N[Notification Service]
EV --> A[Analytics Service]
EV --> S[Search Indexer]
end
This diagram shows orchestration and choreography composing in one flow. The critical core - charge, verify, confirm - runs under an orchestrator that owns the strict ordering. When payment is confirmed, the orchestrator emits a PaymentConfirmed event. From there, the flow becomes choreographed: the notification service, analytics service, and search indexer each react to that event independently, with no one directing them. The orchestrator controls the critical sequence and then hands off to choreographed downstream services through an event - this is where orchestration and choreography meet in practice.
This is the mental model that unlocks real distributed systems: orchestrate the critical core, then let choreographed services react to the events it emits. The orchestrator owns what must be controlled (the payment sequence); the event it emits fans out to everything that just needs to react (notifications, analytics, indexing). You get tight control where it matters and loose, scalable decoupling for the downstream effects - in the same flow. The event is the seam where orchestration hands off to choreography.
A worked example: a refund, orchestrated then choreographed
Trace a refund end to end to see the composition in motion. A customer requests a refund, and the critical core runs under orchestration: the orchestrator validates the refund is allowed, reverses the payment through the gateway, and updates the order status to "refunded" - validate → reverse → update, in strict order, fully traceable. If reversing the payment fails, the orchestrator handles it precisely (retry, or flag for manual review) rather than leaving the order in a broken half-refunded state. This part must be controlled: money is moving, and you need to know exactly what happened.
Once the refund is confirmed, the orchestrator emits a RefundCompleted event - and the flow hands off to choreography. The notification service hears the event and emails the customer "your refund is on its way." The analytics service hears it and updates refund metrics. The inventory service hears it and restocks the returned item. The loyalty service hears it and adjusts points. None of these needs to happen in a strict order, none is business-critical to the second, and each scales independently. The orchestrator neither knows nor cares who's listening - it just announced the fact, and the choreographed services took it from there.
One refund, two coordination styles, joined by one event. The money-critical reversal is orchestrated and auditable; the downstream fan-out is choreographed and decoupled. This is the shape of nearly every important flow in a large system - a controlled core that emits an event, and a loosely-coupled periphery that reacts to it. Once you see this pattern, you see it everywhere.
Designing the event seam
The event the orchestrator emits is the seam where control hands off to decoupling, and designing it well is its own small discipline worth understanding.
A good handoff event is a fact, not a command. The orchestrator emits PaymentConfirmed or RefundCompleted - a statement that something happened - not SendConfirmationEmail, which would be telling a specific service what to do. This distinction matters enormously: a fact is something any number of services can react to however they like, now or in the future, without the orchestrator knowing about them. A command couples the orchestrator to a specific consumer and a specific action. By emitting facts, the orchestrator stays cleanly decoupled from the choreographed services - it announces reality and moves on, and services subscribe to the facts they care about. This is exactly what keeps the "add a new reaction by adding a new listener" flexibility intact: a new service can start reacting to PaymentConfirmed without the orchestrator ever being modified.
A good handoff event also carries enough context for consumers to act without calling back. If the PaymentConfirmed event includes the order ID, amount, and customer reference, the notification and analytics services can do their work straight from the event, rather than each having to query back for details (which would recouple them and add load). The art is including enough for consumers to be self-sufficient, without bloating the event. Get the event right - a well-named fact carrying sufficient context - and the seam between orchestration and choreography is clean, and the whole hybrid stays loosely coupled where it should be.
The hybrid reality: most systems use both
This composition is why real-world production systems don't choose one style over the other - they use both, strategically. The payment and order lifecycle is orchestrated for control and reliability. The downstream effects of those events - notifications, analytics, indexing - are choreographed for scalability and decoupling. The orchestrated core emits events; the choreographed periphery consumes them.
This hybrid gives you the best of both worlds: tight control where it matters, flexible scalability where it doesn't. And it's not a compromise - it's the correct design, because the two styles are suited to different parts of the same system. The critical path is orchestrated and traceable; the high-volume fan-out is choreographed and independently scalable; and an event connects them. That's what a mature distributed system actually looks like.
Important: The insight that separates a beginner from an experienced distributed-systems engineer: orchestration and choreography are not a binary choice you make once for your whole system. They're tools you apply per flow by criticality - and they compose within a single flow, because the orchestrator emits events that choreographed services consume. "Events alone make you scalable" is false; "orchestrate the critical core and choreograph the reactions to its events" is how you actually get both control and scale. The event emitted by the orchestrator is the hinge on which the whole hybrid turns.
Decision checklist: orchestration or choreography?
A practical checklist for classifying any flow:
Choose orchestration when:
- Failure requires immediate compensation (you must undo precisely and promptly).
- The flow has conditional branching or complex logic (many "if this, then that" paths).
- You need full audit trails and step-by-step visibility (traceability is required).
- Ordering of steps is critical (the sequence must be guaranteed).
Choose choreography when:
- Services should be independently deployable and scalable (loose coupling matters).
- The flow is linear and reactions are simple (no complex branching to track).
- Eventual processing is acceptable (a slight delay causes no harm).
- You want to avoid a central bottleneck (no single coordination chokepoint).
Run each flow through this, and the right coordination style becomes clear - and you'll often find yourself orchestrating one part and choreographing the reactions to it, which is exactly right.
Interview: The sophisticated answer to "orchestration or choreography?" is "it depends on the flow's criticality - and they compose." Explain the framework (critical/strict-ordering/traceability → orchestration; non-critical/loosely-coupled/eventual → choreography), give concrete examples (payment and order lifecycle orchestrated; notifications, search indexing, analytics choreographed), and then deliver the insight that impresses: the orchestrator emits events that choreographed services react to, so a single flow can be orchestrated at its critical core and choreographed in its downstream reactions. Showing you understand the two as composable - not a binary choice - signals genuine distributed-systems maturity.
What's next: handling duplicate events
There's a lurking problem in all of this that we've quietly ignored - and it's critical. This entire design runs on events: the orchestrator emits PaymentConfirmed, services react. But what happens when a single event is raised twice?
In distributed systems, duplicate events are inevitable. Network retries resend messages, message brokers redeliver events to be safe, and service restarts can cause reprocessing. So the notification service might receive PaymentConfirmed twice - and send two confirmation emails. The analytics service might count the same purchase twice. Worse, a payment-related handler might act twice on one event. If every service isn't built to handle duplicates, this hybrid architecture corrupts data at scale.
The solution is idempotency - ensuring that processing the same event twice produces the same result as processing it once. It's what makes event-driven systems safe in the face of the duplicates that distributed systems inevitably produce, and it's exactly what we'll tackle next. (For a head start, see the dedicated idempotency guide.)
The bottom line
Using events doesn't make a system scalable - using them well does, and that hinges on who controls each flow. The framework is criticality-based: critical flows (payment, refunds, order lifecycle) need a central orchestrator that owns strict ordering, traceability, and failure handling; non-critical flows (notifications, search indexing, analytics) suit choreography, where services react to events independently and scale on their own. But the deeper truth is that these aren't opposing choices - they compose. The orchestrator runs the critical core and emits events that choreographed services consume, so a single flow is orchestrated where it must be controlled and choreographed where it can be decoupled. That hybrid - tight control at the core, scalable reactions at the edges, joined by an event - is how real systems are built.
And it all rests on events, which means it all rests on handling duplicate events correctly - the inevitable retries and redeliveries of distributed systems - through idempotency. That's next. We cover coordination, event-driven design, and idempotency across System Design Fundamentals.
Go deeper
- Saga pattern (microservices.io) - orchestration and choreography as coordination styles.
- Event-driven architecture (Wikipedia) - how services react to events.
- Orchestration (computing) (Wikipedia) - centralized coordination of workflows.
- Designing Data-Intensive Applications - messaging, coordination, and reliability in distributed systems.