Interview Prep Zoneby CuriouserLabs

Question 1 of 5 · staff level

Walk through two-phase commit — and explain exactly what people mean when they call it blocking.

🎤 Say this first

2PC turns 'all commit or none do' into a vote run by a coordinator. Phase 1 (prepare): the coordinator asks every participant can you commit?; each does the whole transaction, takes its locks, force-writes a prepare record, and answers YES or NO. A YES is a promise — that participant has given up its right to abort unilaterally. Phase 2 (commit/abort): the coordinator force-writes the decision to its own log — that write is the decision — and broadcasts it; participants apply it and release locks. The blocking problem is the gap between those phases: if the coordinator dies after the votes but before the decision arrives, participants that voted YES may not abort (they promised) and have not been told to commit, so they sit holding locks until the coordinator recovers and reads its log. No timeout fixes it — timing out means guessing, and a wrong guess leaves one service committed and another rolled back.

The full picture

🎬 Two-phase commit: the gap where it blocks

1 / 5
one transaction, three databasesCoordinatortransaction T1log: emptyOrdersidlePaymentsidleInventoryidlePREPAREThe coordinator's log is the decision. Phase 2 only tells people about it.

One business transaction, three services, three separate databases. There is no shared lock manager, so 'all commit or none do' has to be negotiated over the network — and two-phase commit is that negotiation, run by a coordinator.

The happy path is two round trips. The interesting path is the coordinator dying in the gap between them.
PhaseCoordinatorParticipantWhat can go wrong
1 — prepareasks all, collects votesdoes the work, locks, force-writes prepare, votesa NO or a timeout → global abort, which is safe
2 — decideforce-writes the outcome, then broadcastsapplies the outcome, releases lockscoordinator dies here → participants block
Recoveryreads its log, re-broadcastsasks the coordinator, or waitscoordinator log lost → only heuristic (manual) resolution
  • The asymmetry is the whole protocol: aborting is always safe, so a timeout in phase 1 can be resolved unilaterally — abort. Committing is never safe to guess, so a timeout in phase 2 cannot. That's why the blocking window is exactly the interval in which some participant has voted YES and does not yet know the outcome.
  • A prepared transaction is a live transaction holding locks, and that is far worse than it sounds. In Postgres, an orphaned PREPARE TRANSACTION holds its locks and pins the vacuum horizon indefinitely — table bloat that no amount of tuning fixes. This is precisely why max_prepared_transactions defaults to 0 and you must deliberately turn the feature on.
  • Heuristic decisions are the operational reality: when an operator is tired of waiting, XA lets a participant resolve on its own — XA_HEURCOM (committed anyway) or XA_HEURRB (rolled back anyway). The transaction manager then reports a heuristic mixed outcome, which means: the atomicity guarantee is gone and a human must reconcile. Any answer that mentions heuristic outcomes signals you've run this in production.
  • Blocking is not a bug in 2PC — it's a theorem. A non-blocking atomic commit protocol requires a perfect failure detector, which an asynchronous network cannot give you; atomic commit is as hard as consensus. 3PC adds a pre-commit phase and is non-blocking under crash-stop with bounded delays, but it breaks under network partitions (both sides can decide differently), which is exactly the failure that actually happens. That's why nobody ships 3PC.
  • Where you'll still legitimately meet 2PC: XA/JTA with Atomikos or Narayana in enterprise stacks (broker + database in one trust domain), Postgres PREPARE TRANSACTION, MySQL XA, and — most importantly — inside modern distributed databases. Spanner, CockroachDB, TiDB and YugabyteDB all run 2PC; they just run it across Paxos/Raft groups rather than single nodes, which is the trick that defuses it.
What the participant's log makes possible — and what it costs
// Phase 1: the participant does EVERYTHING except make it visible.
tx.doWork();                 // rows written, constraints checked
tx.lock();                   // locks acquired and HELD until phase 2
log.forceWrite(PREPARED);    // fsync #1 — survives a crash, so the promise is durable
vote(YES);                   // from here on it may NOT abort on its own

// ---- coordinator decides, force-writes its own log, broadcasts ----

// Phase 2:
log.forceWrite(COMMITTED);   // fsync #2
tx.applyAndReleaseLocks();

// Two round trips and two fsyncs per participant, and locks are held across
// BOTH — so p99 latency is the slowest participant, and lock contention is the
// slowest participant plus a network round trip. That cost, not the protocol's
// complexity, is why 2PC does not scale out.

🔄 Likely follow-up questions

  • Why can a participant abort on a phase-1 timeout but not on a phase-2 timeout?
  • What is a heuristic decision, and what does it cost you?
  • 3PC is non-blocking on paper — why does nobody use it?