You book a holiday: flight, hotel, rental car — three separate bookings made one after another. The flight and hotel go through, but the car company has nothing available. You can't "rewind time" to before you booked the flight; that booking really happened. What you can do is cancel the flight and the hotel — new actions that undo the earlier ones. That's a compensating transaction.
The problem
Inside a single database, undo is easy: an ACID transaction either commits everything or rolls it all back, and a failed step vanishes as if it never ran. But a business operation that spans multiple services or databases can't be wrapped in one transaction. Each step commits to its own store the moment it succeeds. So when step three fails, steps one and two are already durably committed — there's no shared transaction manager to roll them back as a unit. You're left holding partial, real, irreversible-by-default progress.
- Committed stepA local transaction that durably commits to its own service the instant it succeeds — it can't quietly vanish later.
- No shared rollbackThere's no transaction manager spanning the services, so a failed third step can't undo the first two as a unit. You're stuck with partial, real progress.
How it works
For every step that changes state, you define a paired compensating step that semantically reverses it: charge payment pairs with refund payment, reserve stock pairs with release stock, book flight pairs with cancel flight. When a later step fails, you walk backward through the steps that already succeeded and run each one's compensation in turn. Crucially, this is not a database rollback — a refund is a brand-new transaction that offsets the original charge; the charge still exists in history. It's a logical undo built from forward actions. The diagram below shows a forward chain committing step by step, a failure on the last step, and the compensations firing in reverse to unwind the completed work.
- Forward stepA local transaction that commits independently to its own service the moment it succeeds.
- CompensationA new, counteracting action that semantically undoes a committed step — a refund offsets a charge.
Make compensations idempotent and retryable. A compensation can itself fail mid-flight, or get triggered twice after a crash. If "refund payment" runs twice it must not refund twice. Lean on idempotency keys and a retry policy so unwinding is as robust as the forward path — an undo that breaks halfway is worse than no undo at all.
When to use it
Compensating transactions are the undo half of a saga: use them whenever a multi-step operation spans services that can't share one ACID transaction and you still need a way to back out of partial progress. They fit long-running workflows — order fulfillment, travel booking, provisioning — where each step is independently committed and any step might fail.
The hard part is that not everything is cleanly reversible. An email already sent or a physical package already dispatched can't simply be cancelled, so some compensations must be approximate (a follow-up correction), a fallback, or a hand-off to a human. If your operation lives in a single database, don't bother — let a plain ACID rollback do the work for free.