Distributed Transactions: 2PC / 3PC & Their Limits
2PC gives atomicity via prepare-then-commit, but a coordinator crash after the vote blocks participants holding locks; harden the coordinator with consensus or use sagas.
No shared log, no single owner
A single-node database transaction is atomic because one process owns the commit decision and one write-ahead log records it. The moment your transaction spans two independently-owned services or two databases, there is no shared log and no single owner, so you need a protocol to make N participants agree to commit or abort together. Two-phase commit (2PC) is that baseline, and every alternative in this module is defined against it.
2PC has a coordinator and participants. Phase 1 (prepare/vote): the coordinator asks every participant "can you commit?" Each participant does the work, writes it durably in a prepared state, locks the affected rows, and votes yes or no. A yes vote is a binding promise: "I will commit if you tell me to, even if I crash and restart." Phase 2 (commit/abort): if all voted yes, the coordinator writes a commit record and tells everyone to commit; if any voted no, it broadcasts abort. This guarantees atomicity: all commit or all abort.
The fatal flaw: blocking
Between voting yes and hearing the decision, a participant holds locks and cannot unilaterally decide. If the coordinator crashes after participants voted yes but before broadcasting the decision, every participant is stuck: it cannot commit (maybe someone voted no) and cannot abort (maybe everyone voted yes and the coordinator already told others to commit). They hold their locks and wait. This is the classic in-doubt window, and it lasts as long as the coordinator is down.
Coordinator P1 P2
|---- prepare ---->| |
|---- prepare ------------------>|
|<---- yes --------| |
|<---- yes ----------------------|
X (crash here) <- P1, P2 now BLOCKED holding locks
| (wait...) (wait...)
The second problem is throughput. Locks are held across the entire protocol: multiple network round trips plus disk forces. A single-node commit holds a lock for microseconds; a 2PC lock is held for milliseconds to seconds across the fleet. Contended rows serialize behind it, so 2PC caps concurrency hard. This is why it does not survive at internet scale.
3PC inserts a pre-commit phase so participants can time out and make progress if the coordinator vanishes, reducing blocking. But it assumes a synchronous network with bounded delays; under a real partition it can violate atomicity (different sides decide differently), so it is almost never used in production.
Interview nuance: modern systems do not abandon 2PC, they harden the coordinator. Spanner and CockroachDB run 2PC but replicate the coordinator's state via Paxos/Raft, so a coordinator crash is just a failover to a replica that knows the decision, and the in-doubt window closes in seconds. XA (the classic 2PC standard) is acceptable within one cluster or trust domain where the coordinator is HA and latencies are bounded. It is a poor fit across independently-deployed microservices, which is exactly why sagas exist.
Recap: 2PC guarantees cross-participant atomicity via prepare-then-commit, but a coordinator crash after the vote leaves participants blocked holding locks, and lock-holding across the whole protocol throttles throughput, so at scale you either replicate the coordinator with consensus or switch to sagas.
Apply
Your turn
The task this lesson builds to.
Design an atomic transfer of $100 across two independently-owned services (an Accounts service and a Ledger service, each with its own database) and explain why classic 2PC is a poor fit, including the exact failure that blocks it.
Think about
- What blocks participants when the coordinator crashes after prepare?
- Why is holding locks across the protocol a throughput killer?
- How do modern systems harden the coordinator?
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Explain how a system like Google Spanner or CockroachDB runs 2PC across shards at global scale without the classic coordinator-blocking problem crippling it, and quantify roughly where the latency goes. Lead with the mechanism that removes the blocking.
Think about
- What makes the coordinator's decision survive its own crash?
- How long is the in-doubt window after the fix, and why?
- Why do architects still keep transactions single-shard when possible?