Senior and architect loops reach for the saga pattern because it's where "just wrap it in a transaction" stops working. Once a business process spans two or more services with their own data stores, there is no ACID transaction across all of them, and interviewers want to know you understand exactly why, not just that you've heard the word "saga." This page works through the pattern in depth: choreography versus orchestration, how to design compensations for steps that can't simply be rolled back, where saga state actually lives, what happens when a participant never replies, and how three concrete .NET implementation paths — MassTransit's saga state machine, NServiceBus's saga model and Durable Functions orchestrations — differ in practice once you get past the pitch.
Q1 Why is two-phase commit avoided in microservices architectures, and what does the saga pattern do instead?#
Short answer: Two-phase commit requires a coordinator to hold every participant's locks while it asks "can you commit?" and then "commit," so every participant blocks for as long as the slowest one — or the coordinator itself through a crash — takes to respond. That's incompatible with services that deploy, scale and fail independently, and many aren't even relational databases capable of joining an XA transaction. The saga pattern replaces one atomic cross-service transaction with a sequence of local transactions, each committed immediately and independently, plus a defined compensating action for every step that can undo its business effect if a later step fails.
The mechanics of why 2PC fails at this scale are specific, and naming them is what separates a strong answer: the prepare phase forces every participant to hold locks until the coordinator's second round trip, so one slow or unavailable participant blocks all the others indefinitely — the classic blocking problem. It also requires every participant to speak the same distributed-transaction coordination protocol, which most message brokers, NoSQL stores and third-party APIs simply don't implement. And it defeats the entire point of splitting the system in the first place: a temporarily unavailable inventory service would now block every checkout, reintroducing exactly the cross-service coupling microservices were adopted to remove.
Sagas trade atomicity for availability. Each step commits on its own, and the system passes through intermediate states other parts of it can observe — an order can be "Placed" before payment is confirmed — which is a real cost that has to be communicated to the business, not hidden behind a UI that pretends the process is atomic. What you get back is that no service is ever blocked waiting on a lock held by another service's transaction coordinator.
What interviewers look for: naming the locking mechanics of 2PC specifically, rather than dismissing it as merely "old-fashioned," and being explicit that sagas give up atomicity in exchange for availability.
Common mistakes: describing 2PC as slow without explaining why its locking model is operationally incompatible with independently deployed services.
Q2 Compare choreography-based and orchestration-based sagas. When would you pick one over the other?#
Short answer: In choreography, each participant publishes an event when it finishes its step and reacts to events from others, with no central coordinator — the saga's sequence is implicit, distributed across every participant's event handlers. In orchestration, a dedicated orchestrator explicitly calls each participant, tracks progress, and decides what happens next, including which compensations to trigger. Choreography fits a short saga with two or three steps where you want to avoid adding a new central component; orchestration fits anything longer or more failure-prone, where you need one place to see and control the whole process.
Choreography's advantage is that it needs no new service — participants that already publish domain events can react directly, with no single coordinating component to build and keep running. But as the number of steps grows, the sequence of the process stops existing anywhere in code; it only exists as an emergent property of every participant's event handlers, which makes the saga hard to visualize, hard to test end to end, and prone to cyclic event dependencies — service A reacting to an event that B raised in response to A's own earlier event — that get genuinely difficult to reason about past a handful of steps.
Orchestration makes the process explicit: one place to read to understand the whole saga, one place to add a step or change ordering, and a natural home for timeout handling and compensation logic instead of spreading it across every participant. The cost is a new component that becomes a dependency for that saga's participants — though it doesn't have to be a runtime single point of failure if its state is durably persisted and it resumes correctly after a crash, which is exactly what MassTransit's saga state machine and Durable Functions both provide. Most architects default to choreography for a two-step reaction and orchestration once there's meaningful compensation logic or a timeout that needs central enforcement.
What interviewers look for: the "sequence exists explicitly versus emergently" framing, and a concrete threshold for choosing one over the other instead of a blanket preference.
Follow-up questions:
- How would you debug a choreography saga that appears to have stalled partway through?
- Can choreography and orchestration coexist in the same system?
Q3 How do you design compensating transactions? What makes a step "compensatable," and what do you do about a step that truly can't be undone?#
Short answer: A compensatable step is one whose business effect can be semantically reversed by another deliberate action, even if that action isn't a literal undo — "reserve inventory" compensates with "release the reservation," "authorize a card" compensates with "void the authorization." A step is not compensatable when its effect is irreversible in the real world — an email already sent, funds already released to a third party outside your control — and the design has to prevent that step from running until every earlier step has definitely succeeded, typically by ordering it last.
Good compensations are semantic reversals, not database rollbacks, because each step already committed its own local transaction. "Undo" means running a new, deliberate operation that produces the opposite effect, and that operation has to be idempotent and safe to run even when the original step's outcome is unclear — for example, after a network timeout on the original call, where you genuinely don't know if it succeeded. This is why saga design typically orders steps by reversibility risk: put the truly irreversible action — charging a card for real, shipping the order, sending the confirmation email — as late as possible, ideally last, after every compensatable step has already succeeded, so the number of things you would ever need to compensate for is minimized.
For steps that are genuinely irreversible and can't be moved to the end, the fallback isn't code — it's a business process: a manual reconciliation queue, a refund workflow, a customer-service escalation. No pattern makes an already-sent email un-sent, and interviewers want to hear that you plan for that explicitly rather than assuming every step has a clean automatic compensation.
What interviewers look for: the "order by reversibility, irreversible step last" heuristic, and an honest acknowledgment that some compensations are business processes, not code.
Common mistakes: assuming every action has an automatic compensation; placing the irreversible step in the middle of the saga instead of at the end.
Q4 How and where should saga state be persisted, and what data does a saga instance actually need to track?#
Short answer: Saga state has to be durable and queryable independently of any single participant, because the saga instance is what survives a crash of the orchestrator process and lets it resume exactly where it left off. In practice that's a dedicated table or document per saga type, keyed by a correlation ID, storing the current step, the data accumulated from completed steps, and enough information to invoke the right compensations if something later fails.
The minimum shape is a correlation ID that ties every message in this saga instance together — often the business entity's own ID, such as the order ID — a current-state marker, a timestamp for when the current step started (needed for timeout detection, covered next), and the data each completed step contributes that later steps or compensations will need. Once inventory is reserved, for instance, you need the reservation ID to release it later, so that has to be part of the persisted saga state, not something implied by "inventory was reserved."
This state store needs the same kind of concurrency control as any other shared mutable state: two messages for the same saga instance arriving close together shouldn't both be allowed to read-modify-write the saga's state unsynchronized. This is why frameworks like MassTransit back saga state with optimistic concurrency — a row-version column checked on every save — and why hand-rolling saga persistence without that protection is a common source of subtle, hard-to-reproduce saga bugs under real concurrent load.
What interviewers look for: the correlation-ID-plus-accumulated-data model, and awareness that saga state needs concurrency control just like any other piece of shared mutable state.
Q5 How do you handle timeouts inside a saga — a participant that never replies?#
Short answer: Schedule an explicit timeout message or timer alongside the request to the participant, and treat "the timeout fires before the reply arrives" as its own event the saga reacts to, typically by starting compensation for whatever already succeeded. You also have to handle the case where the original reply and the timeout race each other, so a late reply arriving after the timeout already triggered compensation doesn't get processed a second time.
A saga step calling out to a participant should never wait synchronously and indefinitely — it sends the request, often as a message, and schedules a timeout using the same durable, resumable mechanism the saga itself relies on, so the timeout survives an orchestrator restart exactly as the rest of the saga's state does. Both the eventual reply and the timeout are simply events the saga's state machine can receive, and whichever arrives first determines the next transition. If the timeout fires first, the saga moves into compensation; if the reply eventually shows up afterward, it has to be recognized as belonging to a saga instance already past that state and ignored, rather than acted on again.
Choosing the timeout duration is as much a business decision as a technical one — too short, and you compensate for, and potentially undo real, in-progress work from a participant that was simply slow; too long, and a genuinely stuck saga sits in an ambiguous, uncommunicated state longer than the business can tolerate. It usually needs to be configurable per saga type, not a single global default.
What interviewers look for: the durable-timer mechanic plus explicit handling of the reply-versus-timeout race, not just "add a timeout."
Follow-up questions:
- How would you distinguish "the participant is just slow" from "the participant already failed and will never reply"?
Q6 Walk through implementing an orchestrated saga with MassTransit's saga state machine.#
Short answer: MassTransit models a saga as a state machine class deriving from MassTransitStateMachine<TInstance>, where TInstance is your persisted saga-state class carrying a CorrelationId plus whatever accumulated data you need. The state machine declares State properties, Event properties correlated to incoming messages, and Initially/During blocks that describe which events are valid in which states and what to do in response — publish a command, transition state, schedule a timeout.
The shape in code is declarative rather than imperative: you describe the state machine's transitions once, and MassTransit's runtime persists the instance — via an Entity Framework Core, MongoDB or other configured repository — between every event, loading it by correlation ID, applying the transition, and saving it back, so the saga survives a process restart mid-flight without any of that persistence code living inside your state machine class.
public class OrderSagaStateMachine : MassTransitStateMachine<OrderSagaState>
{
public State AwaitingInventory { get; private set; } = null!;
public State AwaitingPayment { get; private set; } = null!;
public State Completed { get; private set; } = null!;
public Event<OrderPlaced> OrderPlaced { get; private set; } = null!;
public Event<InventoryReserved> InventoryReserved { get; private set; } = null!;
public Event<InventoryReservationFailed> InventoryFailed { get; private set; } = null!;
public Event<PaymentCaptured> PaymentCaptured { get; private set; } = null!;
public OrderSagaStateMachine()
{
InstanceState(x => x.CurrentState);
Initially(
When(OrderPlaced)
.Then(ctx => ctx.Saga.OrderId = ctx.Message.OrderId)
.PublishAsync(ctx => ctx.Init<ReserveInventory>(new { ctx.Message.OrderId }))
.TransitionTo(AwaitingInventory));
During(AwaitingInventory,
When(InventoryReserved)
.PublishAsync(ctx => ctx.Init<CapturePayment>(new { ctx.Saga.OrderId }))
.TransitionTo(AwaitingPayment),
When(InventoryFailed)
.TransitionTo(Completed)); // nothing to compensate — the reservation never happened
}
}What interviewers look for: understanding the state-machine-as-declaration model — events, states and correlated persistence handled by the framework — rather than expecting a hand-rolled switch statement over an enum.
Q7 How does NServiceBus model sagas, and how does it differ from MassTransit's approach?#
Short answer: NServiceBus models a saga as a class deriving from Saga<TSagaData>, implementing IAmStartedByMessages<TMessage> for whichever message can begin the saga and IHandleMessages<TMessage> for every message it reacts to afterward, plus a ConfigureHowToFindSaga method that maps an incoming message's properties to the saga's correlation property. It's a more imperative, handler-method style than MassTransit's declarative state-machine DSL, though both persist saga data durably and both are built around the same underlying model of correlated messages driving a long-running process.
The practical difference shows up in how "current step" is represented. MassTransit's state machine makes state a first-class property with named values and explicit During(state, ...) blocks that only accept certain events while in that state, enforced by the framework. An NServiceBus saga's current step is typically a field on the saga data class that your handler methods read and branch on manually — more flexible, but it puts more of the "is this event valid right now" logic in your hands rather than the framework's. Both support timeouts as a first-class concept — NServiceBus via a RequestTimeout call from within a handler — and both persist saga instances to a configurable durable store keyed by a correlation property.
In practice, the choice between them is often less about saga semantics specifically and more about which transport and tooling ecosystem a team has already standardized on. NServiceBus has historically leaned toward enterprise service-bus-style tooling out of the box, while MassTransit is more commonly paired directly with a broker like RabbitMQ or Azure Service Bus with a lighter footprint. Interviewers are generally more interested in whether you understand the underlying saga concepts than in brand loyalty to either framework.
What interviewers look for: confirming both are viable, understanding the state-machine-versus-handler-methods stylistic difference, and not overstating a functional gap between them that isn't really there.
Q8 How would you implement a saga using Durable Functions instead of a message-broker-based framework? What are the trade-offs?#
Short answer: A Durable Functions orchestrator function expresses the saga as ordinary-looking sequential C# code, awaiting one activity call per step, while the Durable Task Framework transparently checkpoints progress after every awaited activity so the orchestrator can replay its event history and resume exactly where it left off after a crash, with no explicit state-persistence code of your own. Compensation is just a try/catch around the sequence, calling compensating activities in the catch block for whichever steps already completed.
[Function(nameof(OrderSagaOrchestrator))]
public static async Task RunOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
{
var order = context.GetInput<OrderRequest>();
var completed = new Stack<string>();
try
{
await context.CallActivityAsync(nameof(ReserveInventory), order);
completed.Push(nameof(ReleaseInventory));
await context.CallActivityAsync(nameof(CapturePayment), order);
completed.Push(nameof(RefundPayment));
await context.CallActivityAsync(nameof(ShipOrder), order);
}
catch (TaskFailedException)
{
while (completed.Count > 0)
await context.CallActivityAsync(completed.Pop(), order);
}
}The trade-off is about where the saga's identity lives and how it talks to the outside world. A message-based framework like MassTransit or NServiceBus fits naturally when participants are themselves independent services already communicating by publishing and subscribing to messages — the saga is just another participant in that same fabric. Durable Functions fits naturally when the orchestration itself is the primary unit of deployment, calling out to activities or external APIs from a serverless workflow, and you're comfortable with the constraints that keep orchestrator code replay-safe: no direct I/O and no non-deterministic calls such as DateTime.Now or Guid.NewGuid() outside the framework's own deterministic equivalents, with every external interaction routed through an activity function.
What interviewers look for: recognizing that Durable Functions gives you saga-like durable orchestration without a broker, at the cost of orchestrator-code determinism constraints message-based sagas don't have.
Follow-up questions:
- What happens if an orchestrator function's code changes while an instance is mid-flight?
Q9 What consistency guarantees does a saga actually give you, and what should you tell a stakeholder who asks "so is it still ACID?"#
Short answer: No — a saga gives you eventual consistency, not ACID atomicity. Each local transaction commits and becomes visible independently, so there's a real window where the system is in an intermediate, in-progress state that other parts of it can observe — an order that shows as placed before payment is actually confirmed, for example. If a later step fails, compensations bring the system back to a consistent end state, but not by making it look as though the failed attempt never happened.
The honest framing for a stakeholder is that a saga trades a global "all or nothing, nobody ever sees a partial result" guarantee for keeping every service independently available and deployable, and that the intermediate states have to be either hidden from the user — don't display "confirmed" until the whole saga completes — or explicitly designed for, treating an order status of "Processing" as a legitimate, expected state rather than a bug. Compensation is also not the same as rollback: a compensated saga has actually done and then undone real actions — a card was authorized and then voided, which is often still visible on a statement even though the net effect is zero — and that distinction matters for anything with an external, human-visible side effect.
This is also where isolation deserves a direct answer: sagas typically provide no cross-step isolation by default, so two saga instances touching overlapping data can interleave in ways a single ACID transaction never would. If that's unacceptable for a specific resource, the fix is an explicit application-level safeguard — a semantic lock, or reserving the resource in the very first step — not an assumption that the saga framework prevents it for you.
What interviewers look for: a direct, confident "no, and here's what eventual consistency actually costs the business," rather than a vague reassurance that a saga behaves basically like a database transaction.
Common mistakes: implying compensation makes it as if the failed attempt never happened, when real actions were actually taken and then reversed.
Q10 How do you test a saga, including its compensation paths?#
Short answer: Test the state machine's transition logic in isolation from any real broker or database — most saga frameworks, MassTransit included, provide an in-memory test harness that lets you publish an event and assert the resulting state and any commands or events produced, without spinning up real infrastructure — and separately write an explicit test for every failure branch that should trigger compensation, not only the happy path, since compensation logic is exactly the code that's rarely exercised in normal operation and most likely to be wrong when it's finally needed.
Happy-path tests confirm the saga reaches its terminal success state given every expected reply, in order. The more important set of tests forces each participant to fail, or time out, at each step in turn, and asserts that exactly the right compensations fire for whatever already succeeded and that nothing fires for steps that never ran. This is where bugs actually hide, because it's easy to write a compensation handler that assumes a prior step's data is always present when, for the specific failure under test, that step never got far enough to produce it.
Beyond the state machine itself, a smaller number of true integration tests — running the saga against a real broker and a real, containerized database for one or two representative scenarios — catch the class of bug unit tests can't reach: serialization mismatches between the message contracts different participants actually use, or a saga persistence mapping that doesn't round-trip correctly. The balance mirrors ordinary microservices testing: many fast, isolated state-machine tests, a handful of realistic integration tests, not the reverse.
What interviewers look for: explicit failure-branch and compensation coverage named as its own test category, not treated as an afterthought to happy-path testing.
Quick-Fire Round#
| Question | Answer |
|---|---|
| Why is 2PC avoided across microservices? | It holds every participant's locks until the slowest one responds, blocking independently deployed services. |
| What replaces atomicity in a saga? | A sequence of local transactions plus compensating actions, with eventual consistency. |
| Choreography or orchestration for a long, failure-prone process? | Orchestration — one place to see and control the whole flow. |
| Where should the irreversible step in a saga go? | Last, after every compensatable step has already succeeded. |
| What must saga state include beyond the current step? | Data later steps or compensations need, such as a reservation ID to release. |
| How should a timeout and a late reply racing each other be handled? | Whichever arrives first wins; the loser is recognized as stale and ignored. |
| What determinism constraint applies to Durable Functions orchestrators? | No direct I/O or non-deterministic calls outside activity functions. |
| Is a saga ACID? | No — eventually consistent, with compensations that undo rather than hide completed actions. |
How to Prepare#
- Be able to explain, in locking terms, exactly why 2PC doesn't fit independently deployed services — not just call it "old."
- Practice the "order steps by reversibility, irreversible action last" heuristic with a concrete example.
- Have a clear model of what saga state must hold beyond "current step," and why it needs concurrency control.
- Rehearse the timeout-versus-late-reply race explicitly — it's a common follow-up probe.
- Be able to sketch both a MassTransit-style state machine and a Durable Functions orchestrator for the same saga, and explain when you'd pick each.
- Bring one testing story that covers a compensation path, not just the happy path.