Why architecture matters here
Two-phase commit matters because it is the canonical, correct answer to a genuinely hard question — atomic commit across independent systems — and because understanding why it is expensive teaches you when to avoid it. The correctness property it guarantees is strong: assuming no permanent failures, all participants reach the same commit-or-abort decision, and no participant ever commits unilaterally. That is what lets a cross-database money transfer be safe: the debit and credit either both land or both roll back, never one without the other. When you truly need distributed atomicity with immediate consistency — and no weaker model will do — 2PC (via XA transactions, or a database's native distributed transaction feature) is the mechanism.
But the architecture matters even more for its costs, because they are structural, not incidental. The first cost is latency and lock duration: a participant that votes YES must hold its locks from the prepare phase until the commit phase — two network round trips plus two durable log writes later. Those held locks block every other transaction that wants the same rows, so 2PC serializes contention across the whole distributed operation and caps throughput on hot data. The second, deeper cost is the blocking problem: 2PC is not fault-tolerant to coordinator failure. If the coordinator crashes after participants have voted YES but before they learn the decision, those participants are in doubt — they promised to commit, so they cannot unilaterally abort, but they don't know whether to commit, so they cannot proceed. They must wait, locks held, for the coordinator to recover and tell them. This is a fundamental limitation (three-phase commit and consensus protocols like Paxos/Raft-based commit exist to soften it, at further cost), and it is why 2PC couples the availability of every participant to the availability of the coordinator.
Third, 2PC matters as a design signal. Its costs are exactly why modern distributed architectures work hard to avoid needing it — favoring single-writer ownership, idempotent operations, sagas with compensations, and the transactional outbox — so that atomicity is achieved through eventual consistency and application-level reconciliation rather than synchronous cross-system locking. Knowing 2PC deeply is what lets you make that trade-off deliberately: use it where strong atomicity is non-negotiable and the contention is low, and design it away where scale and availability matter more than immediate cross-system consistency.
It is worth being precise about what 2PC does and does not guarantee, because that distinction drives every decision around it. 2PC guarantees atomicity across participants — a shared all-or-nothing outcome — but it is not a consensus protocol and does not, by itself, tolerate the coordinator failing at the worst moment; that is the blocking problem. It also says nothing about isolation between global transactions, which each participant enforces locally through its own locks, nor about durability beyond the requirement that prepared state and the decision be forced to disk. Conflating 2PC with a fully fault-tolerant distributed transaction is the root of much disappointment: it is a correct atomic-commit protocol under the assumption that failed nodes eventually recover, and its famous weakness is exactly the case where that assumption is violated for the coordinator. Understanding that boundary is what tells you to invest in coordinator availability and to route around 2PC when availability, not just correctness, is the priority.
The architecture: every piece explained
Top row: the protocol skeleton. The coordinator (transaction manager) drives the whole thing and owns the decision. Phase one — prepare: the coordinator sends a prepare request to every participant. Each participant does everything short of committing — applies the changes to its transaction log in a prepared record, keeps its locks, and forces that record to durable storage — then votes. A YES vote is a binding promise: 'I have durably prepared; I can commit whenever you say, and I will not lose this even if I crash.' A NO vote (or a timeout, treated as NO) means it cannot. Phase two — decide: if all votes are YES the coordinator commits; otherwise it aborts. It sends the decision to every participant, which then finalizes (commit makes the prepared changes visible and releases locks; abort rolls them back and releases locks) and acknowledges.
Middle row: durability, the thing that makes 2PC survive crashes. The coordinator log is where the decision becomes real: the coordinator writes 'commit' (or 'abort') to its own durable log before notifying anyone. That log record is the authoritative outcome — after it exists, the transaction will commit no matter who crashes, because recovery replays it. The participant log holds the prepared state on disk, so a participant that votes YES and then crashes comes back up still prepared, locks conceptually re-established, waiting to be told the outcome. Locks held from prepare to decide are the price of the guarantee — the data is frozen for the duration. The in-doubt window is the dangerous interval: a participant has voted YES but not yet heard the decision; if the coordinator is unreachable, the participant is stuck.
Bottom rows: surviving and unsticking failures. The recovery protocol is what happens after a crash: on restart, the coordinator reads its log — any transaction with a decision is re-sent to participants; any without is aborted (the 'presumed abort' optimization) — and participants that were prepared query the coordinator for the outcome and act on it. Heuristic resolution is the emergency escape hatch: when a participant has been blocked in-doubt too long and the coordinator won't recover, an operator (or a timeout policy) makes a unilateral commit-or-abort decision to release the locks — accepting the risk that it might contradict the coordinator's eventual decision, which is then a heuristic mismatch that must be reconciled by hand. The ops strip captures the reality: tuning timeouts, alerting on blocked/in-doubt transactions, making the coordinator highly available, and — most importantly — knowing when to avoid 2PC altogether.
End-to-end flow
Follow a cross-database funds transfer: move $100 from an account in database A to an account in database B, which share no log. A transaction manager acts as coordinator. It begins a global transaction and enlists both A and B. The application performs the debit against A and the credit against B within this global transaction — but nothing is committed yet.
Phase one. The coordinator sends prepare to A. A checks the debit is valid (sufficient funds, no constraint violation), writes a prepared record for the $100 debit to its log, forces it to disk, keeps the row lock on the account, and votes YES. The coordinator sends prepare to B; B validates the credit, logs it prepared, holds its lock, and votes YES. Both promises are now durable: even if A or B crashes this instant, they will come back up remembering they are prepared and holding their intent. Decision. Both voted YES, so the coordinator forces a commit record to its own log. This is the moment of truth — the transfer is now guaranteed to complete. Phase two. The coordinator sends commit to A and B; each finalizes the prepared change (the debit and credit become visible), releases its locks, and acknowledges. The money moved atomically across two databases.
Now the crash that defines 2PC. Rewind to just after both participants voted YES, but before the coordinator wrote its commit record — and the coordinator's machine dies. A and B are prepared, holding locks on both accounts, waiting. They cannot abort (they promised YES) and cannot commit (they weren't told to). Every other transaction touching those two accounts blocks behind the held locks. This is the in-doubt window made real. Recovery: when the coordinator restarts, it reads its log, finds no decision record for this transaction, and under presumed-abort tells A and B to roll back; they release locks and undo the prepared changes, and the transfer safely didn't happen. But suppose the coordinator had already logged 'commit' before dying — then on recovery it re-sends commit and the transfer completes. Either way the durable coordinator log resolves the ambiguity correctly. The genuinely bad case is a coordinator that stays down: A and B remain blocked indefinitely, and eventually an operator invokes heuristic resolution — say, heuristically committing on B to free critical locks — gambling that it matches the coordinator's eventual decision. If it later turns out the coordinator aborted, B has a heuristic mismatch: money credited that should not have been, now a manual reconciliation. That risk — blocked resources and heuristic mismatches when the coordinator is unavailable — is the price 2PC pays for cross-system atomicity, and the reason architects route around it whenever eventual consistency is acceptable.