Lesson 43 of 129
Distributed Transactions: Two-Phase and Three-Phase Commit
A distributed transaction updates data on more than one node (different databases, shards, or services) and must be atomic: either every participant commits or every participant aborts. The problem is called atomic commitment. It is hard because any node or message can fail at any point, and a participant that has voted to commit cannot later change its mind on its own.
Two-phase commit (2PC)
2PC uses one coordinator and several participants (also called resource managers or cohorts). It is implemented by the XA standard, by database engines for cross-shard transactions, and by systems such as Google Spanner (2PC across Paxos groups).
Phase 1: prepare (voting)
- The coordinator sends
PREPAREto all participants. - Each participant checks that it can commit: locks acquired, constraints satisfied, changes durably written to its log. It writes a
preparedrecord to its write-ahead log, then repliesYESorNO. - After voting
YES, a participant is in doubt: it must keep its locks and must not commit or abort until it learns the decision.
Phase 2: commit or abort
- If all participants voted
YES, the coordinator writes acommitrecord to its own durable log (the commit point) and sendsCOMMITto all. If any votedNOor timed out, it logsabortand sendsABORT. - Each participant applies the decision, releases locks, and sends an acknowledgment.
- The coordinator can then forget the transaction.
sequenceDiagram
participant C as Coordinator
participant P1 as Participant 1
participant P2 as Participant 2
C->>P1: PREPARE
C->>P2: PREPARE
P1-->>C: YES (prepared, locks held)
P2-->>C: YES (prepared, locks held)
Note over C: log COMMIT (commit point)
C->>P1: COMMIT
C->>P2: COMMIT
P1-->>C: ACK
P2-->>C: ACKParticipant state machine
stateDiagram-v2
[*] --> Working
Working --> Prepared: PREPARE received, vote YES
Working --> Aborted: vote NO or PREPARE timeout
Prepared --> Committed: COMMIT
Prepared --> Aborted: ABORT
Committed --> [*]
Aborted --> [*]The Prepared state is the dangerous one: the participant can leave it only through a message from the coordinator (or by asking other participants).
In the cooperative termination protocol, an in-doubt participant asks the others for the outcome. If any participant has received COMMIT or ABORT, or has not yet voted (and therefore can abort), the asker learns the outcome. If every reachable participant is also in Prepared, none can decide and all remain blocked.
Failure cases
| Failure | Outcome |
|---|---|
| Participant fails before voting | Coordinator times out and aborts. |
| Participant fails after voting YES | On restart it reads its log, finds prepared, and asks the coordinator for the decision. |
| Coordinator fails before deciding | Participants that have not voted can abort. Participants in Prepared must wait. |
| Coordinator fails after logging the decision, before sending it | Participants stay in doubt until the coordinator recovers and resends. |
| Message loss | Handled by timeouts and retries. COMMIT and ABORT must be idempotent. |
Blocking
2PC is a blocking protocol. If the coordinator crashes after participants voted YES and before they learn the outcome, those participants cannot safely decide alone. If they commit, another participant may have been told to abort. If they abort, the coordinator may have decided to commit. They must hold locks until the coordinator recovers. This can stall other transactions that need the same rows.
Consequences:
- Availability depends on the coordinator. It is a single point of failure for in-doubt transactions unless its log is replicated (Spanner replicates the coordinator's state with Paxos, which removes this weakness).
- Lock hold time. Locks are held across at least two network round trips plus disk forces at every participant.
- Latency. At least two sequential message rounds and multiple synchronous log writes per transaction.
- Heuristic decisions. Operators sometimes force an in-doubt participant to commit or abort manually, which risks inconsistency.
Optimizations include presumed-abort and presumed-commit (fewer log writes and messages in the common case) and read-only participants that skip phase 2.
Three-phase commit (3PC)
3PC adds a middle phase so that no participant is in a state from which it cannot infer the outcome, making the protocol non-blocking under crash failures and a reliable network.
- CanCommit: coordinator asks; participants vote.
- PreCommit: if all voted yes, the coordinator sends
PRECOMMIT. Participants acknowledge. A participant inPreCommitknows every participant voted yes. - DoCommit: coordinator sends
COMMIT.
If the coordinator fails, participants elect a new coordinator, which polls their states. If any participant has committed, the outcome is commit. If any participant is in PreCommit, the new coordinator completes the pre-commit and commits. If all are still uncertain (voted yes, no PRECOMMIT), it aborts, because no participant can have committed before every participant received PRECOMMIT. Timeouts drive failure detection.
Limitations of 3PC
- It assumes a synchronous network with bounded message delay and no partitions. In a partition, one side may time out and commit while the other times out and aborts, breaking atomicity.
- It adds another round trip, so latency increases.
- It is rarely used in practice. Systems that need non-blocking atomic commit generally use 2PC where the coordinator and participants are themselves replicated with consensus, such as Paxos Commit or Spanner.
Why not use distributed transactions across services
Microservices normally own their data (see database per service). Cross-service 2PC is avoided because:
- Locks span service boundaries, coupling availability and latency of every service in the transaction. If participant failures are independent, the availability of the transaction is roughly the product of the participants' availabilities.
- Many stores and message brokers do not support XA or a prepare phase (most NoSQL databases, HTTP APIs, SaaS providers).
- Long-running business processes cannot hold locks for seconds or minutes.
- The coordinator becomes a shared component that couples teams.
The alternative is the saga pattern: a sequence of local transactions, each committing immediately, with compensating transactions that semantically undo earlier steps on failure. Sagas give up isolation and atomicity in the strict sense (intermediate states are visible) in exchange for availability and loose coupling. Reliable event publishing alongside a local transaction is handled by the transactional outbox.
Comparison
| Property | 2PC | 3PC | Saga |
|---|---|---|---|
| Atomicity | Yes | Yes (with assumptions) | Eventual, via compensation |
| Isolation | Yes, through locks | Yes, through locks | No (needs countermeasures) |
| Blocks on coordinator failure | Yes | No, under crash-stop and no partitions | No |
| Message rounds | 2 | 3 | One local transaction per step |
| Partition tolerant | Safe but blocks | Unsafe | Available |
| Typical use | Within one database or replicated coordinator | Mostly theoretical | Cross-service business processes |
Interview questions
- Why is 2PC called a blocking protocol? A participant that voted YES cannot commit or abort on its own. If the coordinator fails before delivering the decision, it must hold its locks until the coordinator recovers.
- What is the commit point in 2PC? The moment the coordinator durably logs the commit decision. Before it, the transaction can still abort. After it, the outcome is commit even if the coordinator crashes.
- How does 3PC avoid blocking, and why is it not widely used? The pre-commit phase guarantees that any participant that has not seen
PRECOMMITcan abort and any that has can commit. This relies on bounded delays and no partitions, which real networks do not guarantee, and it costs an extra round trip. - What should a service do instead of 2PC across microservices? Use a saga with compensating actions, and use idempotent steps and an outbox for reliable messaging, accepting eventual consistency.
