The Saga Pattern: How Microservices Fake Transactions They Can't Actually Have


Ask any database to move money from account A to account B and it will wrap the debit and credit in a transaction, commit both or neither, and never show you a half-finished state. That guarantee is one of the most useful things a database does for you. Split that same operation across two services with two separate databases, and the guarantee quietly disappears. This is the problem the saga pattern exists to work around, and understanding why reveals a lot about how distributed systems actually behave under the hood.

Why Two-Phase Commit Didn’t Win

The obvious fix is to make a multi-service operation transactional the same way a single database does: a coordinator asks every participant to prepare, waits for everyone to agree, then tells them all to commit. This is two-phase commit (2PC), and it does technically work. The problem is what happens when things go wrong.

2PC requires every participant to hold locks on its data from the moment it agrees to prepare until the coordinator’s final commit message arrives. If the coordinator crashes, dies, or gets partitioned away mid-protocol, participants are stuck holding those locks indefinitely, unsure whether to commit or roll back. In a system with a handful of services this is a nuisance. In a system with dozens of services, unreliable networks, and independent deploy cycles, it becomes a availability liability that can lock large parts of a system in response to a single failure. Most teams building distributed systems at scale decided that blocking correctness was worse than the alternative: giving up atomicity and dealing with the consequences directly.

How Sagas Work Instead

A saga breaks a multi-step operation into a sequence of local transactions, one per service, each of which commits immediately and independently. There is no global lock and no coordinator holding everyone hostage. If step three fails, the saga doesn’t roll back a transaction, because there was never one transaction to roll back. Instead it runs compensating actions: a sequence of steps that semantically undo the effects of the steps that already succeeded.

Booking a trip is the standard example. Reserving a flight, then a hotel, then a rental car are three separate local transactions against three separate systems. If the car reservation fails, the saga doesn’t have a “rollback” button. It issues a cancellation against the hotel and a cancellation against the flight, each of which is its own local transaction with its own success or failure path.

Orchestration vs Choreography

Sagas are typically implemented in one of two styles. In orchestration, a central coordinator service explicitly calls each step and decides what compensating actions to trigger on failure. This keeps the logic in one readable place but reintroduces a central component that everything depends on, which is exactly what teams often adopt sagas to avoid.

In choreography, each service reacts to events published by the previous one and emits its own event when done, with no central coordinator at all. This scales better organizationally since services stay decoupled, but the overall business process ends up implicit, scattered across event handlers in different codebases. Debugging “why didn’t my order ship” means tracing an event chain across several services instead of reading one function.

Compensating Actions Aren’t Free

The part that trips people up is that compensation is not the same as rollback. A database rollback is guaranteed to leave no trace. A compensating action is a business operation, and business operations can fail, arrive out of order, or interact with the outside world in ways that can’t be undone. You can cancel a hotel reservation, but you can’t uncharge a customer’s card without issuing a refund, which is a different operation with its own failure modes, delays, and edge cases. Every compensating step needs to be idempotent, because retries and duplicate events are a normal part of operating a distributed system, not an exceptional case.

When You Actually Need This

Sagas are a solution to a specific problem: coordinating state changes across services that each own their own data and can’t share a transaction. If your operation touches one database, use that database’s transactions and skip the added complexity entirely. Sagas earn their cost specifically when service boundaries are drawn around independent ownership and independent failure domains, which is most of the time the actual justification for having separate services in the first place.