A teammate says a saga can just roll everything back if step four fails. What is wrong with that, and what would you tell them to build instead?
A saga has no rollback. Each step already committed in its own database and was visible to everyone, so you can only run new forward transactions that semantically offset it. Compensations are not atomic, some actions cannot be compensated at all, and compensations themselves fail - so a saga needs idempotent handlers and an ordering rule for irreversible steps.
What the interviewer is scoring
- Whether the candidate distinguishes discarding uncommitted work from issuing a new transaction that offsets committed work
- That they recognise each saga step committed independently and was already observable to other services
- Does the answer name at least one action that cannot be compensated, and say how the design accommodates it
- Whether compensations are required to be idempotent and retryable, given that they run in the same unreliable network
- That the candidate raises the isolation gap - other transactions can read the intermediate state a saga passes through
- Whether they can say what happens when a compensation itself fails permanently, rather than assuming it will not
Answer
Short answer
A saga has no rollback, because there is nothing left to roll back. Each step committed in its own database and became visible to every other service the moment it did. What a saga offers instead is compensation: new forward transactions that semantically offset earlier ones. Compensation is not atomic, some actions cannot be compensated at all, and the compensating transaction can itself fail — which is why "just roll it back" is not a design.
What rollback actually means
ROLLBACK in a single database discards work that was never committed. The write-ahead log is used to restore the pre-transaction state, and — crucially — no other transaction ever saw the discarded state, because isolation prevented it. Rollback is cheap and invisible precisely because nothing depended on the work being undone.
None of that holds across services. By the time step four fails, steps one through three have each run COMMIT in a different database. Payment has a settled authorisation. Inventory has a decremented count. An OrderPlaced event is sitting in Kafka and three consumers have already acted on it. There is no uncommitted state to discard and no log that spans those systems. The word "rollback" is doing damage here, because it implies a mechanism that does not exist and hides the work the team actually has to do.
Compensation is a new transaction, not an undo
The saga pattern replaces rollback with a compensating transaction for each step: an operation that moves the system to a state equivalent enough to never having run the original. The gap between "equivalent enough" and "identical" is where the real design lives.
A refund is not the reverse of a charge. The money moves back, but the customer's statement now shows two entries, the payment processor charged a fee on both, and in some schemes the original authorisation hold persists for days. Releasing reserved inventory is not the reverse of reserving it, because another customer may have seen the item as out of stock in between and left. Cancelling an account is not the reverse of creating it if a welcome email already went out.
// Orchestrated saga: each completed step pushes its compensation onto a stack.
var completed = new ArrayDeque<Compensation>();
try {
var order = orderService.create(cmd);
completed.push(() -> orderService.markCancelled(order.id()));
var charge = paymentService.charge(order.total(), cmd.card());
completed.push(() -> paymentService.refund(charge.id())); // NOT an un-charge
inventoryService.reserve(order.lines());
completed.push(() -> inventoryService.release(order.lines()));
shippingService.dispatch(order.id()); // step four fails here
} catch (StepFailedException e) {
while (!completed.isEmpty()) {
compensateWithRetries(completed.pop()); // must be idempotent and durable
}
}
The stack is the easy part. Everything difficult is inside compensateWithRetries.
The steps you cannot compensate
Some actions leave the system entirely. You cannot unsend an email, unsend an SMS, un-charge a customer's card in a way they will not notice, or un-dispatch a pallet that is on a lorry. The standard response is to classify steps and then order them deliberately.
Steps are grouped into compensatable ones, which can be offset; a pivot step, which is the point of no return; and retryable ones after the pivot, which are guaranteed to succeed eventually and therefore never need compensating. The design rule that falls out is to push every compensatable step before the pivot and every irreversible step after it. If dispatching physical goods is irreversible, dispatch must come after payment and reservation have succeeded, not before — so that when something fails, the failure lands in the region of the saga that can still be offset.
If a teammate is proposing "roll everything back", the fastest way to make the problem concrete is to ask which step in their saga is the pivot. Usually nobody has decided, which means the irreversible step is sitting in an arbitrary position.
Compensations run in the same unreliable network
A compensation is a distributed call with all the failure modes of the original. It can time out, return ambiguously, or land twice when retried. That forces two requirements that "just roll back" never surfaces.
Compensations must be idempotent. refund(chargeId) called three times must refund once. That usually means the compensating handler keys on a stable identifier — the charge id, the saga id, or an explicit idempotency key — and records that it has already run.
Compensations must be durable and retried indefinitely. A compensation that fails cannot simply be dropped, because dropping it leaves the customer charged for an order that will never ship. In practice the saga log is persisted before each step, retries run with backoff, and anything still failing after a bounded number of attempts moves to a dead-letter queue with an alert and a documented manual procedure. The uncomfortable truth worth saying in an interview is that a saga's final error handler is sometimes a human with a runbook, and a design that pretends otherwise is incomplete.
The isolation that ACID gave you and a saga does not
Sagas sacrifice the I in ACID, and this is the part candidates most often miss. A saga passes through intermediate states that are visible to everyone, including states that are semantically illegal — an order that is paid but has no inventory reserved, or inventory reserved against an order that is about to be cancelled. A concurrent transaction can read that state and act on it, and no compensation can retract a decision another system already made from it.
The countermeasures have names worth knowing. A semantic lock marks the record with an in-progress status so readers know not to trust it. Commutative updates let operations be applied in any order so interleaving does not matter. Re-reading the value before acting detects that it changed underneath you. Which one applies depends on the read patterns, but naming the problem at all is a strong signal — it shows the candidate understands a saga is not merely a slower transaction but a weaker one.
What to say to the teammate
The productive framing is not "you are wrong" but "rollback is free and compensation is a feature you have to build". For every step, someone has to write the compensating operation, make it idempotent, decide how long to retry it, and decide what happens when it gives up. For at least one step, the honest answer will be that it cannot be compensated, which forces a conversation about ordering that the team needs to have anyway.
If the interviewer pushes on whether two-phase commit would have avoided all this — it would provide the atomicity, and it is usually unavailable because it requires every participant to support XA, holds locks across the whole exchange, and blocks indefinitely if the coordinator dies mid-protocol. That trade is why sagas exist, and being able to state it is what separates knowing the pattern from having chosen it.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- Step four fails and its compensation also fails after all retries. What happens next?
- Which of your steps are irreversible, and how does that change the order you run them in?
- A customer reads their order status midway through the saga and sees a state that never legally existed. How do you handle that?
- When would you choose orchestration over choreography for this, and what does that cost you?
- Would two-phase commit have solved this? Why is it usually not on the table?
Related questions
- How do you decide where one service ends and the next begins, and when is a modular monolith the more honest answer?hardAlso on microservices and saga7 min
- Two teams own two services that talk to each other. How do you stop one breaking the other without running both together in a shared environment?mediumAlso on microservices4 min
- Two users edit the same record while offline and both reconnect. How do you reconcile the two versions without silently losing one of the edits?hardAlso on eventual-consistency6 min
- Walk me through what happens between a customer order being captured and the service being live on the network.hardAlso on saga5 min