Distributed transactions (2PC and sagas)
All-or-nothing across services, or a sequence of steps with compensations.
01The intuition
The problem it solves
Buying a phone online means three things happen together: your card is charged, one phone is taken from the warehouse count, and an order is created. In one database this is a transaction: all three writes commit, or none do, and nobody sees a half-done state. Now the payments live in one service with its own database, the inventory in another, orders in a third. Charge succeeds, inventory fails because the last phone just went to someone else. You have taken money for nothing. A single database transaction cannot span three services; the question is what replaces it.
Two families of answer exist. Two-phase commit makes the services vote and then commit together, like a wedding ceremony where everyone says "I do" before anyone is married. A saga gives up on all-at-once and instead runs the steps in order, with a written undo for each, like booking a holiday: if the hotel falls through after the flight is booked, you cancel the flight.
Build it one piece at a time
Step 1: two-phase commit (2PC). A coordinator asks each service to prepare: do everything except the final commit, lock the rows, write to disk that you are ready, and reply yes or no. If every reply is yes, the coordinator writes "commit" to its own log and tells everyone to commit. One no, and everyone aborts.
| Phase | Payments | Inventory | Orders | Coordinator |
|---|---|---|---|---|
| prepare | yes (card authorised, held) | yes (1 phone reserved) | yes (order row pending) | all yes |
| commit | capture | decrement | order confirmed | logs commit, done |
The cost: rows stay locked across the network round trips, and if the coordinator crashes between the votes and the decision, every participant sits locked, unable to commit or abort, until it returns. This blocking is why 2PC is used inside a tightly controlled system (a sharded database) and rarely between independent services.
Step 2: a saga, step by step. Run the steps as separate local transactions in sequence: reserve inventory, charge card, create order. Each step publishes an event when done, and the next step listens for it.
Step 3: compensations. For each step write an undo: release the reservation, refund the charge, cancel the order. If step 3 fails, run the compensations for steps 2 and 1 in reverse. The system passes through visible intermediate states (money taken, order not yet created) and must tolerate them for seconds.
| Step | Action | Result | Compensation run |
|---|---|---|---|
| 1 | reserve 1 phone | ok | — |
| 2 | charge card | ok | — |
| 3 | create order | fails (service down) | refund charge, release phone |
Step 4: make every step idempotent and the saga durable. A step may be retried after a crash, so each carries the saga ID as an idempotency key. The saga's own state (which step is done) is stored durably, either by an orchestrator service that drives the steps or by the events themselves in a log, so a crash resumes rather than restarts.
Step 5: the outbox, so an event is never lost. A step writes its database row and its outgoing event in the same local transaction, into an outbox table; a relay publishes outbox rows to the queue. Writing the row and then publishing separately can lose the event between the two, which silently stalls the saga.
Why it works, with the numbers
2PC costs two round trips to every participant and holds locks for both:
lock hold time ≈ 2 × round trip + disk writes ≈ 4–10 ms in one data center, 200+ ms across regions
which caps throughput per hot row at a few hundred transactions a second across regions. A saga holds no cross-service locks; each step is a local transaction of a millisecond or two, and the whole thing completes in the time of the slowest queue hop. The guarantee of 2PC is atomicity: all or nothing, nobody sees the middle. The guarantee of a saga is weaker and must be said: every started saga eventually either completes or is fully compensated. The failure modes: a blocked 2PC after a coordinator crash, a compensation that itself fails (retry forever, then a human), and a compensation that cannot truly undo (an email already sent).
The trade-off you must say out loud
Atomicity and isolation against availability and throughput. Say which you chose and why: 2PC when the participants are shards of one system under one operator, a saga when they are independent services. For a saga, say the compensations, the visible intermediate states and how long they last, and whether an orchestrator or choreography drives it. The follow-up is "what if the refund fails", and the answer is a retry with backoff, an alert, and a ledger that shows the money is owed.
Where it shows up in the 100 Google problems
- Cross-service transactions: the question itself; the saga with an outbox is the expected shape.
- E-commerce site: reserve, charge, order, with compensations when stock runs out.
- Google Pay: a transfer is a two-step saga across account shards, with the ledger as the record of truth.
- Globally consistent database: 2PC between Paxos groups is how a multi-shard transaction commits, with the coordinator itself replicated so it cannot block.
- Uber: matching a driver and reserving them for a rider is a short saga with a release if the rider cancels.
02Used in these problems
- Design a globally consistent, multi-region database (Spanner)
- Design Uber
- Design Ticketmaster
- Design an e-commerce site
- Design an inventory management system
- Design Google Pay
- Design the Play Store revenue split and developer payout flow
- Design Google Calendar
- Design cross-service transactions (sagas and 2PC)
- Migrate a monolith to microservices