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.
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()));
inventoryService.reserve(order.lines());
completed.push(() -> inventoryService.release(order.lines()));
shippingService.dispatch(order.id());
} catch (StepFailedException e) {
while (!completed.isEmpty()) {
compensateWithRetries(completed.pop());
}
}
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 (opens in a new tab).