Distributed transactions — 2PC, 3PC, Saga, TCC
Four ways to make N services commit together — with the trade-offs each accepts.
You're booking a trip online. The click of a "Book" button triggers three separate services: reserve the flight, hold the hotel room, charge the credit card. All three must succeed together — otherwise the customer has a hotel with no flight, or a charge with no reservations. This is a distributed transaction: multiple systems, one atomic outcome.
Local transactions are easy. A single database vendor spent 40 years perfecting ACID for you. Distributed transactions are hard. There are 4 industrial approaches, each with a distinct trade-off: 2PC (Two-Phase Commit), 3PC (Three-Phase Commit), Saga, and TCC (Try-Confirm/Cancel). The choice depends on how much you value blocking vs eventual consistency, and whether you own all the services or integrate with third parties.
The papers
- Gray (1978) — "Notes on Database Operating Systems." The paper that formalized 2PC.
- Skeen (1981) — 3PC, an attempt to fix 2PC's coordinator-crash blocking.
- Garcia-Molina & Salem (1987) — the Saga paper: "Long Lived Transactions" at SIGMOD. Introduced the compensating-transaction pattern.
- Pat Helland (2007) — "Life Beyond Distributed Transactions." The essay that convinced the industry to move away from 2PC.
Two-Phase Commit (2PC) — the classic
The oldest and most widely-implemented distributed transaction protocol. There's a designated coordinator and a set of participants (the services holding the data being modified). The protocol has two phases:
- Phase 1: Prepare. Coordinator asks each participant "can you commit?". Each participant does all the work except the actual commit, writes the transaction to its WAL as "prepared", and replies YES/NO. A NO from anyone aborts the transaction.
- Phase 2: Commit / Abort. If all YES, coordinator writes "commit" to its own log, then tells everyone to commit. If any NO, coordinator writes "abort" and tells everyone to roll back.
2PC is a blocking protocol. If the coordinator crashes between phase 1 and phase 2, participants are stuck holding locks with no way to decide whether to commit or abort — they must wait for the coordinator to recover and read its durable log. This is the single biggest reason 2PC has fallen out of favor for microservices.
Three-Phase Commit (3PC) — the fix that isn't
Skeen (1981) added a third phase ("pre-commit") to avoid the blocking problem. The idea: if you can guarantee that any participant knowing about the commit means at least one other participant does too, then survivors can pick up where the coordinator left off. In theory this makes 3PC non-blocking.
In practice, 3PC doesn't work in real networks. It assumes a synchronous network with bounded message delivery — an assumption real WANs violate constantly. A partition + slow message can still cause split-brain. Most databases quietly dropped 3PC support in the 90s. It exists mainly as a textbook exercise.
Saga — the microservices favorite
Garcia-Molina & Salem 1987. The idea: forget global atomicity. Instead, break the transaction into a sequence of local transactions, each with a compensating transaction that undoes it. Run them one by one. If any step fails, run the compensating transactions for the completed steps in reverse.
Sagas are eventually consistent — there's a window where partial state exists. But they never block, they scale to arbitrary numbers of services, and they don't require the services to trust each other or share a coordinator. This is the de facto pattern for microservices today.
Two flavors: choreography (each service emits events, others react — no central controller) and orchestration (a workflow engine like AWS Step Functions, Temporal, or Uber Cadence explicitly drives the sequence). Orchestration is easier to reason about; choreography scales more organically.
TCC — Try, Confirm, Cancel
A hybrid of 2PC and Saga. Each participant exposes 3 endpoints per operation: Try (reserve resources, no commit), Confirm (actually commit the reservation), and Cancel (release the reservation).
Coordinator calls Try on every participant. If all succeed, it calls Confirm on each. If any fails, it calls Cancel on those that succeeded. The advantage over 2PC: Try holds a business-level reservation (like "hold 2 seats for 5 minutes") instead of a database lock, so it's non-blocking at the DB level. The advantage over Saga: strict atomicity (all confirm or all cancel).
Which one to pick — decision matrix
| Dimension | 2PC | 3PC | Saga | TCC |
|---|---|---|---|---|
| Atomicity guarantee | Strong (all commit or all abort) | Strong (in theory) | Eventual (window of partial state) | Strong (with business locks) |
| Blocks on failure? | ❌ YES (blocks on coordinator crash) | ✓ NO (in theory) | ✓ NO | ✓ NO (business locks, not DB) |
| Coordination overhead | Medium (2 round trips) | High (3 round trips) | Low (async messages) | High (3 endpoints per svc) |
| Requires DB support? | ✓ YES (XA/JTA) | ✓ YES | ❌ NO | ❌ NO |
| Cross-organization? | ❌ NO (need PREPARE endpoint) | ❌ NO | ✓ YES | ✓ YES |
| Compensating logic? | ❌ NO (DB rolls back) | ❌ NO | ✓ YES (write your own) | ✓ YES (in Cancel) |
| Latency | Bounded by slowest participant | Higher than 2PC | Sum of all steps | 2× the slowest step |
| Real users | Postgres XA, Spanner (on Paxos) | Rarely used | Uber, Netflix, Airbnb | Alibaba, financial services |
Why 2PC fell out of favor for microservices
In 2007 Pat Helland (then at Microsoft, formerly at Amazon) wrote the essay "Life Beyond Distributed Transactions." His argument: at internet scale, 2PC just doesn't work. The reasons:
- Coordinator failure blocks participants. Participants hold locks until the coordinator recovers. In a microservices world where any service could restart at any moment, this leads to cascading unavailability.
- Latency scales with slowest participant. With 10 services, your commit latency is bounded by the slowest of the 10.
- Cross-organization impossible. Nobody exposes "PREPARE" to strangers. You can't 2PC with Stripe, or Twilio, or your customer's SAP instance.
Helland's conclusion: embrace "entities" that maintain their own atomicity locally and communicate via idempotent messages. This is the intellectual foundation of modern Sagas and event-driven architectures.
Applied in real systems
Postgres — 2PC still exists
Postgres implements 2PC via PREPARE TRANSACTION. Used mostly by monolithic apps spanning multiple databases behind a single JVM (via XA/JTA). Almost never used in microservices.
Google Spanner — Paxos + 2PC
Spanner runs 2PC on top of Paxos groups. Every participant is itself a Paxos-replicated log. This means the coordinator never truly loses state — its Paxos group survives. Spanner is the only 2PC-based system at internet scale that works.
Temporal — the modern saga engine
Temporal (spun off from Uber Cadence) is the industry-standard workflow orchestration engine. Every saga step is a workflow activity; compensations are just other activities. Handles retries, timeouts, versioning, human intervention.
AWS Step Functions — saga in JSON
State-machine-based saga orchestration. Cheap for low volume, expensive per transition at high volume. Great for "fire and forget" workflows; less good for latency-critical paths.
Kafka — exactly-once with transactions
Kafka has an internal 2PC-lite for producers: transactional producers atomically write to multiple topic-partitions. Consumers see either all writes or none. Used by Kafka Streams for exactly-once processing.
Seata — AT/TCC/Saga in one framework
Open-sourced by Alibaba after running distributed transactions across Taobao at massive scale. Offers 4 modes: AT (auto-compensate via SQL undo logs), TCC (manual Try/Confirm/Cancel), Saga, and XA. The industrial buffet.
Key takeaways
- 2PC: atomic but blocking on coordinator crash. Fine within a monolith, fatal across microservices.
- 3PC: theoretically non-blocking, in practice broken by real networks. Rarely used.
- Saga: eventually consistent, non-blocking, scalable. Requires designing compensating transactions. The microservices default.
- TCC: business-level reservations (non-blocking DB locks) + strict atomicity. Requires each service to implement 3 endpoints per operation.
- Spanner uniquely makes 2PC work at internet scale by running it on top of Paxos-replicated logs.
- Compensating transactions ≠ rollback. A compensation is a new transaction that undoes the effect — "refund the payment" is the compensation for "charge the card".
References
- Gray (1978) — Notes on Database Operating Systems.
- Skeen (1981) — Three-Phase Commit.
- Garcia-Molina & Salem (1987) — Sagas. SIGMOD.
- Helland (2007) — Life Beyond Distributed Transactions.
- Corbett et al. (2012) — Google Spanner. OSDI.
- Chris Richardson — Microservices Patterns, chapter 4 (Saga pattern).
Practice what you just read
Every foundation concept has a companion quiz to close the loop.