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

  1. CanCommit: Coordinator asks if participants can commit (same as 2PC prepare)
  2. PreCommit: If all vote yes, coordinator sends pre-commit (participants know commit is coming)
  3. 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

About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.