Two-Phase Commit

Two-phase commit is the atomic commit protocol that lets a coordinator and a set of participants agree to apply one distributed transaction, or agree to apply none of it. The transaction commits only if every participant votes YES; a single NO aborts everyone, including the participants that already voted YES.

Picture three clerks and a floor manager. The manager asks, "Can you do your part of transfer T?" A clerk who says yes puts a lock on the row and writes the new value in pencil. Nobody inks it yet. If the manager later says COMMIT, the pencils become ink. If anyone said no, every pencil mark is erased. The expensive part is the yes: after that answer, a clerk cannot decide alone. Maybe a colleague voted no. Maybe the manager already said COMMIT and the message is still on the stairs. If the manager vanishes in that window, the clerks keep the locks and wait.

How two-phase commit locks without applying

The cast is a star, not a cluster. The coordinator talks to each participant. Participants never talk to each other. P1, P2, and P3 are three resource managers, not three replicas of one row: P1 owns account A, P2 owns account B, P3 owns the audit log. That is why a prepared participant cannot take a vote among its peers, and why a majority of YES votes is the wrong rule.

Phase 1 is prepare. The coordinator broadcasts PREPARE. Each participant runs a local check. A YES writes a prepare record, holds locks, and keeps the current value on the row with the proposed value beside it. A NO leaves the row unlocked. The coordinator waits for every vote. Two YES votes are not a decision. Three YES votes still are not a commit until the coordinator names one.

Phase 2 is the decision. Unanimous YES becomes COMMIT. Any NO becomes ABORT. The same decision goes to every participant. On COMMIT the proposed value replaces the current one and the lock leaves. On ABORT the proposed value is discarded and the original current value stays. A YES voter that later receives ABORT still rolls back. That is the whole point of "atomic."

The blocking window opens at YES and closes when the decision arrives. Inside it, a participant cannot commit locally (another vote may have been NO) and cannot abort locally (the coordinator may already have decided COMMIT). The default run pauses there on purpose: C is already COMMITTED, P1 is still PREPARED, A is still 100. If the coordinator vanishes in that window, the cohort waits with locks held.

Two-phase commit step by step

The default All yes → commit run starts with A = 100, B = 50, and log = []. Transaction T is "debit A $30, credit B $30, write T in the log."

The proof beats are 7, 9, 10, and 12. The rest is the protocol getting those four facts on screen.

  1. Four idle columns. Coordinator C is IDLE. P1, P2, and P3 are WORKING. The tally strip is C's log, empty, and the notch after slot 3 already says need ALL YES.
  2. T arrives. C becomes WAITING. No lock has moved. A, B, and the log are unchanged.
  3. PREPARE to P1. P1 is still WORKING.
  4. PREPARE to P2. Independent delivery. Zero votes so far.
  5. PREPARE to P3. Phase 1's questions are all out. The answers have not started.
  6. P1 votes YES. The local check is 100 - 30 = 70. P1 locks A and shows proposed 70 in purple. Current A stays 100. Votes = 1 YES / 3.
  7. P2 votes YES. B is 50 -> 80, locked. Votes = 2 YES / 3. The notch is not crossed. C is still WAITING.
  8. P3 votes YES. The log is [] -> [T], locked. Votes = 3 YES / 3.
  9. C decides COMMIT. The third YES is the evidence. No COMMIT message is on the wire. Resources stay purple.
  10. Blocking window. C has COMMIT. P1 is still PREPARED. A is still 100. P1 cannot commit (other votes are unknown to it) and cannot abort (C may already have COMMIT). This is the wait, named on the default run.
  11. COMMIT to P1. P1 is still PREPARED while the card travels.
  12. P1 applies. A snaps 100 -> 70, the lock leaves, P1 is COMMITTED. Applied = 1 / 3.
  13. COMMIT to P2.
  14. P2 applies. B becomes 80. Applied = 2 / 3.
  15. COMMIT to P3.
  16. P3 applies. The log becomes [T]. Applied = 3 / 3.
  17. Done. A = 70, B = 80, log = [T]. Every YES, then every apply. Two YES votes would not have been enough.

Load One no → abort next. A starts at 20, so P1's debit fails and votes NO. P2 and P3 still vote YES and lock. C decides ABORT. The strip counts resolved, not applied: the decision reached everyone, the transfer did not. P2 rolls 80 back to 50. The YES voters abort because atomicity is all-or-nothing, not majority.

Load Coordinator crash last. It copies the default through the COMMIT decision, then C vanishes before any COMMIT leaves. The strip is C's log, now gone; 3 YES / 3 is not a fact P1 can read. P1's view is "I know only my YES; decision not received." P1-P3 stay PREPARED with locks. They wait. This preset does not recover the coordinator on purpose.

Why two-phase commit waits

Raft can commit on three of five. 2PC cannot commit on two of three. The jobs are different. Raft is replicating one log so any majority overlap carries the history forward. 2PC is applying one transaction to several different resources. If the audit log says no, you do not want A and B to move anyway.

You can put the coordinator's decision in a Raft log so the coordinator box is not a single point of failure. That copies the COMMIT record. It does not let two of three resource managers commit while the third said no. Raft majority is not a substitute for 2PC unanimity.

That is also why a YES is expensive. The participant has promised to accept whichever decision arrives. It cannot un-promise just because the coordinator is late. Timeouts-as-NO, presumed abort, 3PC, and cooperative termination exist because people got tired of that wait. They are not this protocol.

When two-phase commit is a fit

Use 2PC when the transaction crosses independently owned resources and you need all of them to move, or none of them, and you can live with locked rows if the coordinator dies. The transfer-plus-audit-log example is the honest case: A and B both changing while the log stays empty is a production incident, not an abstract inconsistency.

Skip it when availability matters more than that atomicity. Compensating steps (a saga) are the usual alternative: each resource commits locally and you unwind later if a later step fails. That is a different promise. Also skip it when the "participants" are really replicas of one log. That is Raft's job.

The bill is linear in the number of resource managers: one PREPARE, one vote, and one decision per participant, with locks held from YES until the decision arrives. PREPARE and vote can go out in parallel, so a larger N is more messages and more locks, not extra serial rounds. This canvas freezes N at 3 so 2 / 3 vs ALL stays readable. A fourth clerk adds another of each message and another lock, not a majority shortcut.

Two-phase commit edge cases

  • Two YES, one NO. The strip reads 2 YES + 1 NO. C aborts. The two locked YES voters roll back.
  • Insufficient funds. Default A = 100 prepares; abort-preset A = 20 votes NO without locking.
  • Decision before apply. After decideCommit, A is still 100. COMMIT is a message, not a global bit.
  • Idempotent ABORT. P1 already refused locally and still receives ABORT. Nothing to roll back.
  • Crash after a private COMMIT. C knows COMMIT. Participants do not. Unilateral apply would happen to be right on this trace and wrong if a NO had existed.

Two-phase commit common mistakes

  • Calling two YES votes a majority commit. N = 3 makes that tempting. The notch is after slot 3. Two never crosses it.
  • Treating a YES as an apply. Purple 70 next to 100 is prepared. The current value moves on applyCommit, not on the vote.
  • Letting a prepared participant decide locally. After YES it cannot commit (a NO may exist) and cannot abort (COMMIT may already be decided).
  • Assuming participants can elect a new coordinator. They never message each other on this canvas. That missing spoke is the blocking story.
  • Confusing this with Raft. Raft's 3 / 5 is a feature of replicating one log. 2PC's need ALL YES is the feature of changing several resources. Swap the thresholds and you break atomicity. Replicating the coordinator does not change the vote rule.

A Note on Simplification

This lesson fixes one coordinator and three participants, and it shows one RPC per beat. Decision ACKs, nested transactions, timeouts treated as NO, presumed-abort optimizations, 3PC, cooperative termination, and coordinator recovery are omitted. The crash preset leaves the cohort blocked instead of replaying a decision log. The vote rule and the blocking window are genuine 2PC; the timing is staged so those two facts stay readable.