Skip to main content
SEMastery

Orchestration vs Choreography in .NET: A Friendly Guide

Orchestration vs choreography explained simply for .NET developers — events, commands, sagas, trade-offs, and when to pick each, with clear C# examples.

13 min readUpdated December 14, 2025

Planning a school function

Imagine your school is holding an annual function. Many small jobs must happen: print the tickets, set up the stage, arrange the snacks, and invite the chief guest. No single person does all four.

There are two ways the teachers can get this done.

In the first way, one head teacher holds a clipboard. She walks around and tells each person exactly what to do and when. "You, print the tickets now." Then she waits. "Good, now you set up the stage." She watches everything and keeps the plan in her head. If the chief guest says no, she decides what to cancel. This is orchestration. One brain is in charge.

In the second way, there is no head teacher. Instead, everyone agreed on cues beforehand. When the tickets are printed, a notice goes up on the board: "Tickets done." The snack team sees that notice and starts cooking. The stage team sees it and starts building. Nobody is told directly; each team just reacts to what they see. This is choreography. Everyone knows their own cue, like dancers on a stage.

Both ways finish the same function. The difference is where the control lives. That is the whole idea of this article. Let us learn it slowly, the .NET way.

The shared problem: one job, many services

In modern .NET systems we split work into small services. Think of an online shop. Placing one order touches several services:

  • Orders service creates the order.
  • Payment service charges the card.
  • Inventory service reserves the stock.
  • Shipping service books the courier.
  • Notifications service sends the email.

Each one has its own database. You cannot wrap all of them in a single database transaction. So you need a way to run these steps across services and still keep things consistent. That coordination is the job of orchestration or choreography.

One business action, many services, each with its own database.

The question is never "should these services talk?" They must. The question is how they coordinate. Read on.

Two words, two pictures

Let us pin down the two words clearly, because people mix them up.

Orchestration means a central service, called the orchestrator, drives the whole flow. It sends commands ("do this") to other services, waits for their replies, and decides the next step. The flow lives in one place.

Choreography means there is no central driver. Each service does its own bit, then publishes an event ("this happened"). Other services listen and react. The flow is spread out, hidden in who-listens-to-what.

The core difference

Orchestration
Choreography

Steps

1

Orchestration

One brain sends commands and tracks progress

2

Choreography

Services react to each other's events, no central brain

Where does the control sit?

A simple way to remember: commands push, events announce. An orchestrator pushes work onto services. A choreographed service just announces a fact and walks away.

Commands vs events

This difference matters a lot, so here is a small table.

AspectCommandEvent
Meaning"Please do this""This already happened"
Sender knows receiver?Yes, one targetNo, anyone can listen
DirectionSender to one servicePublisher to many subscribers
Naming styleVerb, like ReservePaymentPast tense, like PaymentReserved
Used mostly inOrchestrationChoreography

Keep this table in mind. The rest of the article builds on it.

How choreography works

Let us walk through our order with choreography first. No central boss.

The Orders service creates the order and publishes OrderPlaced. It does not know who cares. The Payment service has subscribed to OrderPlaced, so it wakes up, charges the card, and publishes PaymentCompleted. The Inventory service subscribed to PaymentCompleted, so it reserves stock and publishes StockReserved. Finally Shipping hears StockReserved and books the courier.

Choreography: each service reacts to the previous event, like a chain of dominoes.

Notice how the arrows are events, not commands. Nobody is in charge. The flow exists only because each service knows which event to listen for.

Here is what publishing an event might look like in .NET. We will use a simple IPublishEndpoint style, which most messaging libraries provide.

public sealed class PlaceOrderHandler
{
    private readonly IPublishEndpoint _bus;
    private readonly AppDbContext _db;
 
    public PlaceOrderHandler(IPublishEndpoint bus, AppDbContext db)
    {
        _bus = bus;
        _db = db;
    }
 
    public async Task Handle(PlaceOrder command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items);
        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);
 
        // Announce a fact. We do not know or care who listens.
        await _bus.Publish(new OrderPlaced(order.Id, order.Total), ct);
    }
}

And here is the Payment service reacting. It listens for OrderPlaced, does its work, and announces the next fact.

public sealed class OrderPlacedConsumer : IConsumer<OrderPlaced>
{
    private readonly IPaymentGateway _gateway;
 
    public OrderPlacedConsumer(IPaymentGateway gateway) => _gateway = gateway;
 
    public async Task Consume(ConsumeContext<OrderPlaced> ctx)
    {
        var result = await _gateway.Charge(ctx.Message.OrderId, ctx.Message.Total);
 
        if (result.Succeeded)
            await ctx.Publish(new PaymentCompleted(ctx.Message.OrderId));
        else
            await ctx.Publish(new PaymentFailed(ctx.Message.OrderId, result.Reason));
    }
}

See the pattern? Each service is small and independent. To add a new step, say a loyalty-points service, you just write a new consumer that listens for PaymentCompleted. You change nothing in the other services. That is the big charm of choreography.

The hidden cost of choreography

But there is a catch. Look at the diagram again. Where is the whole flow written down? It is not anywhere. It lives scattered across many services. To understand the full order journey, you must open five projects and trace who publishes what and who subscribes to what.

This gets worse when something fails. If stock reservation fails, who undoes the payment? In choreography, Inventory must publish StockReservationFailed, and Payment must have a consumer for it that issues a refund. The undo logic is also scattered. With many services and many events, this can turn into a tangle people call event spaghetti.

Choreography failure path

StockFailed
RefundPayment
CancelOrder

Steps

1

StockReservationFailed

Inventory publishes the bad news

2

Refund

Payment hears it and refunds

3

Cancel

Orders hears it and cancels

Undo logic is spread across services too.

How orchestration works

Now the same order with orchestration. We add one service: the OrderSaga orchestrator. It is the head teacher with the clipboard.

The orchestrator receives the request and then drives every step. It sends ReservePayment to Payment and waits. When Payment replies, it sends ReserveStock to Inventory and waits. Then BookShipping to Shipping. The orchestrator keeps the current state in a database row, so it always knows where the order is.

Orchestration: one orchestrator sends commands and waits for each reply.

Every arrow goes through the orchestrator. The flow is all in one place. You can open the OrderSaga class and read the whole story top to bottom.

Here is a tiny hand-written orchestrator to show the idea. Real apps use a library, but this makes the concept clear.

public sealed class OrderOrchestrator
{
    private readonly ISendEndpointProvider _send;
    private readonly ISagaStore _store;
 
    public OrderOrchestrator(ISendEndpointProvider send, ISagaStore store)
    {
        _send = send;
        _store = store;
    }
 
    public async Task Start(StartOrder cmd, CancellationToken ct)
    {
        var saga = new OrderSagaData(cmd.OrderId) { State = "AwaitingPayment" };
        await _store.Save(saga, ct);
 
        // Send a direct command to one known service.
        await _send.Send(new ReservePayment(cmd.OrderId, cmd.Total), ct);
    }
 
    public async Task On(PaymentReserved msg, CancellationToken ct)
    {
        var saga = await _store.Load(msg.OrderId, ct);
        saga.State = "AwaitingStock";
        await _store.Save(saga, ct);
 
        await _send.Send(new ReserveStock(msg.OrderId), ct);
    }
 
    public async Task On(PaymentFailed msg, CancellationToken ct)
    {
        var saga = await _store.Load(msg.OrderId, ct);
        saga.State = "Cancelled";
        await _store.Save(saga, ct);
        // No compensation needed yet; nothing later happened.
    }
}

Notice that the orchestrator uses commands (ReservePayment, ReserveStock) and saves a state after each step. That saved state is what lets it survive restarts and know exactly where to continue.

Compensation is easier to follow

When a step fails halfway, the orchestrator already knows which earlier steps ran. So it can send the matching undo commands in order. Because the rollback plan lives in one class, it is much easier to read and test.

Orchestrated rollback: the orchestrator walks backward, sending undo commands.
public async Task On(StockReservationFailed msg, CancellationToken ct)
{
    var saga = await _store.Load(msg.OrderId, ct);
 
    // We know payment already succeeded, so undo it.
    await _send.Send(new RefundPayment(msg.OrderId), ct);
 
    saga.State = "Compensating";
    await _store.Save(saga, ct);
}

Side by side comparison

Here is the honest trade-off, in one table.

QuestionOrchestrationChoreography
Where is the flow written?One place, the orchestratorSpread across many services
Coupling between servicesHigher, all know the orchestratorLower, services only know events
Easy to read and debug?Yes, single storyHarder, must trace events
Easy to add a new step?Edit the orchestratorAdd a new listener, touch nothing else
Single point of failure?The orchestrator, needs careNo central point
Best forLong flows, rollback, orderingSimple reactions, loose coupling
RiskOrchestrator becomes a god classEvent spaghetti at scale

There is no winner. The right choice depends on your flow. A useful rule of thumb: if the steps must happen in a strict order and need careful rollback, lean toward orchestration. If the steps are independent reactions that can fan out freely, lean toward choreography.

You can mix both

Real systems rarely pick just one. A common and healthy pattern is to orchestrate the core money flow and choreograph the side effects.

For example, the OrderSaga orchestrates payment, stock, and shipping because those must be correct and ordered. But once the order is confirmed, the orchestrator publishes a single OrderConfirmed event. Then the email service, the loyalty service, and the analytics service each choreograph off that event. They are side effects; order does not matter, and you add new ones freely.

A hybrid: orchestrate the core, then choreograph the side effects.

This gives you the clarity of orchestration where you need control, and the looseness of choreography where you want easy growth.

.NET tools you can reach for

You can build both styles by hand with RabbitMQ or Azure Service Bus clients. But libraries save a lot of plumbing, especially for orchestrated sagas with saved state.

  • MassTransit has a clean saga state machine for orchestration. Note that from version 9 it became a commercial, source-available product with a free local evaluation license, so check pricing before production.
  • NServiceBus also offers sagas and has long been a commercial product.
  • Wolverine and Rebus are other strong .NET choices for messaging and sagas.
  • Azure Durable Functions and Dapr workflows give you durable orchestration without writing the state store yourself.

For pure choreography you often need less: just a publish-subscribe bus and well-named events. The simpler your need, the lighter the tool.

Here is a small taste of an orchestrated saga defined with a state machine style, so you can see how libraries express the same idea more declaratively than our hand-written class.

public sealed class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public State AwaitingPayment { get; private set; } = null!;
    public State AwaitingStock { get; private set; } = null!;
 
    public Event<OrderStarted> Started { get; private set; } = null!;
    public Event<PaymentReserved> PaymentReserved { get; private set; } = null!;
 
    public OrderStateMachine()
    {
        InstanceState(x => x.CurrentState);
 
        Initially(
            When(Started)
                .Then(c => c.Saga.Total = c.Message.Total)
                .Send(c => new ReservePayment(c.Saga.CorrelationId, c.Saga.Total))
                .TransitionTo(AwaitingPayment));
 
        During(AwaitingPayment,
            When(PaymentReserved)
                .Send(c => new ReserveStock(c.Saga.CorrelationId))
                .TransitionTo(AwaitingStock));
    }
}

Read it top to bottom and the whole flow is visible. That readability is the heart of why teams pick orchestration for complex processes.

A simple way to choose

When you are unsure, ask yourself these short questions.

  • Does the process have a clear start and end with strict ordering? Lean orchestration.
  • Do failures need careful, ordered rollback? Lean orchestration.
  • Are the steps independent reactions that do not care about order? Lean choreography.
  • Will you add new reactions often without touching old code? Lean choreography.
  • Is the team finding it hard to see the whole flow? You may have over-used choreography; consider an orchestrator.

Decision helper

Ordered?
Rollback?
Pick

Steps

1

Strict order?

If yes, prefer orchestration

2

Needs rollback?

If yes, orchestration is safer

3

Loose reactions?

If yes, choreography fits

A quick path to a sensible default.

Common mistakes to avoid

A few traps catch teams again and again.

First, do not turn the orchestrator into a god class that knows every tiny detail of every service. Keep it focused on the flow, and let each service own its real logic.

Second, do not use choreography for a process that truly needs order. If three events can arrive in any sequence and break things, you are fighting the wrong tool.

Third, remember that both styles still need the usual messaging safety nets: idempotent consumers so duplicate messages do not double-charge, the outbox pattern so you never lose a published event, and retries with backoff for flaky calls. These are not optional in distributed systems.

Fourth, name your messages honestly. Commands should be verbs (ShipOrder) and events should be past tense facts (OrderShipped). Mixing these up confuses everyone reading the code.

Quick recap

  • Orchestration uses one central orchestrator that sends commands and tracks the whole flow in one place.
  • Choreography has no central boss; each service publishes events and others react.
  • Commands push a known service to act; events announce a fact to anyone listening.
  • Orchestration is easier to read, debug, and roll back, but couples services to the orchestrator.
  • Choreography is loosely coupled and easy to extend, but the full flow is hidden and can become event spaghetti.
  • Both are ways to run a saga, a multi-step process where each step has an undo called a compensation.
  • Mixing both is common: orchestrate the core flow, choreograph the side effects.
  • In .NET, reach for MassTransit, NServiceBus, Wolverine, Rebus, Azure Durable Functions, or Dapr — and recall that MassTransit and NServiceBus are commercial.
  • Whichever you pick, still use idempotency, the outbox pattern, and retries.

References and further reading

Related Patterns