Topic Overview
Three-Phase Commit (3PC)
Learn Three-Phase Commit protocol that reduces blocking compared to 2PC.
Three-Phase Commit (3PC) extends 2PC with a "pre-commit" phase to reduce blocking when the coordinator fails.
The Problem with 2PC
In 2PC, if the coordinator fails after participants vote "yes" but before sending commit, participants are blocked - they don't know whether to commit or abort.
3PC Solution
Adds a pre-commit phase so participants know the decision before committing.
Phases
- CanCommit: Coordinator asks if participants can commit (same as 2PC prepare)
- PreCommit: If all vote yes, coordinator sends pre-commit (participants know commit is coming)
- DoCommit: Coordinator sends commit, participants commit
Key insight: If coordinator fails in phase 2, participants know commit was decided and can safely commit.
Implementation
class ThreePhaseCommit {
async execute(participants: Participant[]): Promise<boolean> {
// Phase 1: CanCommit
const votes = await Promise.all(
participants.map(p => p.canCommit())
);
if (!votes.every(v => v === 'yes')) {
await this.abort(participants);
return false;
}
// Phase 2: PreCommit
await Promise.all(participants.map(p => p.preCommit()));
// Phase 3: DoCommit
await Promise.all(participants.map(p => p.doCommit()));
return true;
}
}
class Participant {
private state: 'initial' | 'prepared' | 'pre-committed' | 'committed' | 'aborted' = 'initial';
async canCommit(): Promise<'yes' | 'no'> {
// Same as 2PC prepare
this.state = 'prepared';
return 'yes';
}
async preCommit(): Promise<void> {
// Know that commit is coming
this.state = 'pre-committed';
// Can prepare for commit but don't commit yet
}
async doCommit(): Promise<void> {
if (this.state === 'pre-committed') {
this.state = 'committed';
// Actually commit
}
}
// Recovery: If coordinator fails, can check state
async recover(): Promise<void> {
if (this.state === 'pre-committed') {
// Coordinator failed but we know commit was decided
// Can safely commit
await this.doCommit();
} else if (this.state === 'prepared') {
// Coordinator failed before pre-commit
// Must abort (don't know decision)
await this.abort();
}
}
}
Common Pitfalls
- Still requires all nodes: Network partitions still problematic
- More complex: Three phases instead of two
- Not widely used: Most systems use 2PC or alternatives like Saga
- Timeout handling: Must handle timeouts in each phase
Interview Questions
Beginner
Q: How does 3PC differ from 2PC?
A: 3PC adds a "pre-commit" phase between prepare and commit:
2PC: Prepare → Commit/Abort 3PC: CanCommit → PreCommit → DoCommit
Benefit: If coordinator fails after pre-commit, participants know commit was decided and can safely commit (non-blocking). In 2PC, they would be blocked.
Intermediate
Q: When would you use 3PC over 2PC?
A: Use 3PC when:
- Coordinator failures are common: Need non-blocking behavior
- Can tolerate extra round trip: 3PC has higher latency (3 phases)
- Need better fault tolerance: Participants can recover without coordinator
However, 3PC is rarely used in practice because:
- Still not partition-tolerant: Requires all nodes reachable
- More complex: Harder to implement and debug
- Alternatives exist: Saga pattern, Paxos/Raft often better choices
Recommendation: Use 2PC for simple cases, or consider Saga/Paxos for better fault tolerance.
Senior
Q: Design a 3PC system that handles coordinator failures. How do participants recover and ensure consistency?
A:
class FaultTolerant3PC {
async executeWithRecovery(participants: Participant[]): Promise<void> {
// Phase 1: CanCommit
const votes = await this.canCommitPhase(participants);
if (!this.allYes(votes)) {
return this.abort(participants);
}
// Phase 2: PreCommit
await this.preCommitPhase(participants);
// Phase 3: DoCommit
await this.doCommitPhase(participants);
}
async handleCoordinatorFailure(): Promise<void> {
// Participants can recover independently
const participants = await this.getParticipants();
for (const participant of participants) {
await participant.recover();
}
}
}
class RecoverableParticipant {
async recover(): Promise<void> {
const state = await this.getState();
switch (state) {
case 'pre-committed':
// Coordinator failed after pre-commit
// We know commit was decided, can safely commit
await this.doCommit();
break;
case 'prepared':
// Coordinator failed before pre-commit
// Don't know decision, must query others or abort
const decision = await this.queryDecision();
if (decision === 'commit') {
await this.doCommit();
} else {
await this.abort();
}
break;
case 'initial':
// Coordinator failed before prepare
// Can safely abort
await this.abort();
break;
}
}
async queryDecision(): Promise<'commit' | 'abort'> {
// Query other participants
const others = await this.getOtherParticipants();
for (const other of others) {
const state = await other.getState();
if (state === 'pre-committed' || state === 'committed') {
return 'commit'; // At least one is pre-committed, decision was commit
}
}
return 'abort'; // No one pre-committed, must abort
}
}
Consistency guarantees:
- Pre-committed state: All participants that reach pre-committed know commit was decided
- Recovery protocol: Participants can query each other to determine decision
- Majority rule: If majority is pre-committed, decision was commit
Key Takeaways
- 3PC reduces blocking: Participants can recover if coordinator fails after pre-commit
- Three phases: CanCommit → PreCommit → DoCommit
- Pre-commit state: Indicates commit decision was made
- Non-blocking recovery: Participants can commit independently if in pre-committed state
- Still not partition-tolerant: Requires all nodes reachable
- Rarely used: Most systems prefer 2PC or alternatives (Saga, Paxos)
- Use when: Need non-blocking behavior but still want strong consistency