Guide

Two-phase commit (2PC) is the classic atomic commit protocol. A coordinator drives a set of participants, each a database or resource manager, so that all of them commit or all of them abort.

Concept

A participant is any resource that holds part of a transaction, such as a database. The coordinator is the component that decides the outcome for all of them.

The two phases

Prepare. The coordinator asks each participant whether it can commit. A participant that votes yes must first write all of the transaction's changes durably to disk, and keep holding its locks. Voting yes is a promise: "I will commit if told to, even if I crash and restart." From that moment, the participant gives up its right to abort on its own.

Commit. If every participant voted yes, the coordinator durably logs a commit decision. That log write is the moment the transaction commits. It then tells everyone to commit. If anyone voted no or didn't answer, the coordinator logs abort and tells everyone to abort. It keeps resending the decision until every participant acknowledges.

The blocking problem

The weak point is the coordinator. Suppose every participant votes yes, and the coordinator crashes before it sends the decision. The participants are in doubt. They promised to commit if told, so they can't abort. They don't know the decision, so they can't commit either. They must wait, holding their locks, until the coordinator recovers and reads its log. Other transactions that touch those rows block too.

If the coordinator's disk is lost, a person may have to decide. This is why 2PC is called a blocking protocol. The availability math is harsh too, since every participant must be up at once:

3 participants, each 99.9% available
P(all up) = 0.999 × 0.999 × 0.999 ≈ 0.997 → 99.7%
That is about 26 hours of downtime a year, instead of about 8.8 hours for one participant.

XA and why it is rare

XA is the X/Open standard, from 1991, for the interface between a transaction manager and resource managers. Java's JTA, MySQL (XA START, XA PREPARE, XA COMMIT) and Postgres (PREPARE TRANSACTION, disabled by default with max_prepared_transactions = 0) support it, and so do some message brokers.

XA is still common in enterprise Java stacks. Across microservices it is rare, for four reasons:

  • Every participant must speak XA. A payment provider's HTTP API does not.
  • Locks are held across network round trips to every participant, which hurts hot rows.
  • A coordinator failure can leave locks held indefinitely.
  • It couples the availability of every participant together.

How distributed SQL changes the picture

Distributed SQL databases, such as Spanner, CockroachDB, TiDB and YugabyteDB, give you ordinary BEGIN ... COMMIT across many machines. They still use 2PC inside, but they fix its two biggest weaknesses.

  • Each participant and the coordinator are consensus groups. A Spanner shard is a Paxos group spread across zones or regions. A coordinator crash becomes a leader election within seconds, and the new leader has the full log, including any prepare or commit decision. See consensus.
  • Timestamps order transactions without global locks. Spanner uses TrueTime, CockroachDB uses hybrid logical clocks, and TiDB gets timestamps from a central oracle. Timestamps allow snapshot reads and a global order of commits. See time, clocks and ordering.

The cost is latency. In a multi-region deployment, writes can take tens to hundreds of milliseconds. Distributed SQL fits relational transactions across more data than one machine holds. It doesn't help across services owned by different teams, or a third-party API.

Common mistakes

  • Assuming 2PC works across any services. Every participant must support it.
  • Ignoring the blocked case. Plan the runbook for in-doubt transactions before you need it.
  • Using XA over HTTP. Most HTTP APIs can't hold a prepared state.

Check yourself

1. A participant votes yes, and then the coordinator crashes. Can the participant abort? No. It promised to commit if told, so it must wait for the decision while holding its locks.

2. Why does 99.9% per service become about 99.7% for three services? Every participant must be up, so the availabilities multiply: 0.999 × 0.999 × 0.999 ≈ 0.997.

Key takeaways

  • 2PC gives atomic commits across participants, using prepare and commit phases.
  • A yes vote is a promise, so a participant can't abort on its own after it votes.
  • A coordinator crash can block participants while they hold locks.
  • Distributed SQL makes the coordinator a consensus group, which removes most of the blocking, at a latency cost.

Further learning

Kleppmann's lecture on two-phase commit shows the protocol, its blocking failure mode, and how fault-tolerant versions use consensus.

Distributed Systems 7.1: Two-phase commitMartin Kleppmann

Next, see the alternative for cross-service work in sagas and compensating actions.