"Charge the card, decrement the stock, create the order — all three or none." Every backend eventually needs a transaction that spans more than one system, and two-phase commit (2PC) is the classic answer: the protocol behind XA, behind distributed SQL commits, behind every "transaction coordinator" you've met in an enterprise stack. It's also a protocol most teams are told to avoid without being told exactly why. Both halves — how it works and where it hurts — fit in one picture:
A real techdiagrams document — download the JSON and import it into the editor via File → Import.
What 2PC actually promises
Atomic commit across independent systems: a transaction touching your orders database, inventory database and payments database either commits everywhere or nowhere — no state where the card was charged but the order doesn't exist. One participant becomes special: the coordinator, which owns the decision and writes it to a durable transaction log.
How it works: a vote, then a verdict
Phase 1 — PREPARE. The coordinator asks every participant: "can you commit this?" Each participant does the real work now — writes the changes durably to its log, acquires the locks — and only then votes. A YES vote is not an opinion; it's a binding promise: "I can commit this even if I crash and restart before you call back." That promise is the heart of the protocol, and the price of it is that a YES-voter must hold its locks and wait.
Phase 2 — COMMIT or ABORT. Unanimous YES → the coordinator logs commit and broadcasts it. Any NO, or any timeout → it logs abort and broadcasts that. Participants finish accordingly and release their locks. Done — and in the happy path, it genuinely works: crash-restart on either side is handled by replaying logs.
The problems — and one is famous
1 · Blocking: the in-doubt participant. Look at the diagram. Payments voted YES, then the coordinator died before delivering the verdict. Payments now cannot commit (the decision might have been abort) and cannot abort (Orders and Inventory may have already committed). It is in doubt — frozen, holding locks, until the coordinator comes back. Every query that touches those locked rows queues behind it. This is the theoretical heart of the matter: with an unreliable coordinator, a YES-voter has no safe unilateral move. The availability of all participants is coupled to the health of one process.
2 · Latency and throughput tax. Two network round trips plus at least two durable log writes (prepare and the decision) sit inside every transaction — and locks are held across the full span, including both network hops. Lock hold time is the enemy of throughput, and 2PC maximizes it by design. Cross-region, this goes from expensive to punishing.
3 · The coordinator is a single point of failure — and pushing it to HA (replicating its log) means solving consensus anyway, at which point you've built half of Raft to protect a protocol you chose to avoid building Raft.
4 · Heuristic outcomes. Real implementations offer an emergency lever: an in-doubt participant times out and unilaterally commits or aborts ("heuristic decision"). This trades the blocking problem for something worse — silent atomicity violation — and every XA-era operator has a story about reconciling one.
(Three-phase commit exists and is worth exactly one sentence: it trades one impossible failure for another and dissolves under network partitions, which is why nobody runs it.)
What we use instead — and where 2PC still lives
Most application-level "distributed transactions" today are redesigned rather than coordinated: sagas (a sequence of local transactions with compensating actions — the checkout template composes one), the transactional outbox (kill the dual-write; make one local commit produce the event), and idempotency so retries replace verdicts. The honest framing: sagas trade 2PC's blocking for visible intermediate states — you're not getting atomicity, you're deciding you can live without it.
But 2PC isn't dead — it moved down a layer, and got a bodyguard. Spanner, CockroachDB and friends run 2PC where each participant (and the coordinator's state) is itself a Raft/Paxos group. The coordinator can't "die" in the old sense — its log is consensus-replicated, so in-doubt limbo is bounded by a leader election, not by ops paging someone. That's the modern verdict in one line: 2PC between fallible machines is a trap; 2PC between consensus groups is how planet-scale databases commit.
Break it yourself
Import the diagram into techdiagrams.net, and the in-doubt scenario stops being lore: kill the coordinator at each step and trace what every participant knows at that instant. The moment where YES-with-no-verdict leaves Payments frozen is the moment 2PC's whole trade-off becomes something you've seen — which is exactly how it should be learned.