By the end of this lesson
  • Deconstruct Phase 1 (Prepare) and Phase 2 (Commit) of 2PC and prove why coordinator crashes cause blocking distributed lock deadlocks
  • Analyze why Three-Phase Commit (3PC) fails in real-world asynchronous networks subject to network partitions
  • Design Saga architectures comparing event-driven Choreography against state-machine Orchestration with forward and backward recovery
  • Implement a production-grade TypeScript Saga Orchestrator engine executing idempotent compensating workflows

1. The Problem & Why It Matters

In a monolithic architecture backed by a single relational database, maintaining transaction atomicity is straightforward:

BEGIN TRANSACTION;
  UPDATE accounts SET balance = balance - 100 WHERE id = 'alice';
  UPDATE accounts SET balance = balance + 100 WHERE id = 'bob';
COMMIT;

The database storage engine locks the rows, appends commit records to the write-ahead log (WAL), and guarantees Atomicity, Consistency, Isolation, and Durability (ACID). If the database crashes mid-flight, recovery rollback routines clean up uncommitted changes.

In modern distributed systems, data is fragmented across microservices, separate database engines, and third-party SaaS APIs:

  • An e-commerce checkout must:
    1. Deduct customer loyalty credits in PostgreSQL.
    2. Reserve warehouse inventory in a MySQL shard.
    3. Authorize payment through Stripe’s external HTTP API.
    4. Generate a shipping label via FedEx.

If Stripe succeeds but FedEx rejects the shipping address, you cannot simply execute ROLLBACK. Stripe already charged the credit card, and PostgreSQL already committed the loyalty credit deduction!

This lesson explores how distributed systems solve atomic commitments across independent nodes: from the strict locking of Two-Phase Commit (2PC) to the eventual consistency of Sagas and the resource reservations of Try-Confirm-Cancel (TCC).


2. Intuitive Mental Model

Imagine booking a vacation requiring an Airline Ticket, a Hotel Room, and a Rental Car:

  • Two-Phase Commit (2PC): You call all three vendors simultaneously. You ask: “Can you hold this seat/room/car?” (Prepare). All three say “Yes” and freeze the reservations. You then tell all three: “Book it!” (Commit). If your phone battery dies while they are on hold, all three vendors refuse to rent those seats/rooms to anyone else, waiting indefinitely for your call (The 2PC Blocking Hazard).
  • The Saga Pattern: You book and pay for the flight immediately. Then you book and pay for the hotel. When you try to book the rental car, all cars are sold out! You cannot undo the past, so you execute Compensating Actions: you cancel the hotel (getting a refund) and cancel the flight (paying a cancellation fee).
  • Try-Confirm-Cancel (TCC): You call the hotel and ask them to put a $100 pre-authorization hold on your card (Try). You do the same for the car. Once all pre-authorizations succeed, you call them back to finalize the charges (Confirm). If one vendor fails, you call the others to release the holds (Cancel).
SYSTEM ARCHITECTURE BLUEPRINTDISTRIBUTED SYSTEMS
Distributed Transactions: 2PC vs. Saga Orchestration vs. TCC
ARCHITECTURAL FLOW · STEP 1 OF 3

1. 2PC Phase 1: Prepare & Lock Acquisition

Coordinator broadcasts PREPARE? to DB Node A and B. Both nodes acquire exclusive row locks in local databases and reply YES.

Architectural Invariant: Isolation Cost: While in PREPARED state, exclusive locks block all concurrent readers and writers on those rows.
ARCHITECTURAL FLOW · STEP 2 OF 3

2. Saga Forward Steps: T1 & T2 Commit Locally

Saga Orchestrator invokes Step 1 (Reserve Seat) and Step 2 (Charge Card). Each step commits immediately in its respective service database without holding cross-service locks.

Architectural Invariant: No Global Lock: Eliminates distributed locking. Downside: other transactions can observe intermediate committed states (dirty reads).
ARCHITECTURAL FLOW · STEP 3 OF 3

3. Step T3 Failure & Backward Compensations (C2, C1)

T3 (Book Hotel) fails due to inventory depletion. Orchestrator triggers semantic backward compensations: C2 (Refund Card) and C1 (Release Seat) to restore system consistency.

Architectural Invariant: Eventual Consistency: The system guarantees that every completed forward step is eventually reversed if the overall workflow fails.

3. Architectural Mechanisms & Concrete Topologies

3.1 Two-Phase Commit (2PC) Mechanics

2PC coordinates atomic commitment across a Coordinator and Participants:

COORDINATOR                         PARTICIPANT 1                PARTICIPANT 2
    |                                     |                            |
    |---- 1. PREPARE -------------------->|                            |
    |---- 1. PREPARE ------------------------------------------------->|
    |                                     | [Locks Rows & Writes WAL]  | [Locks Rows & Writes WAL]
    |<--- 2. VOTE_COMMIT (YES) -----------|                            |
    |<--- 2. VOTE_COMMIT (YES) ----------------------------------------|
    |
[Writes COMMIT to WAL]
    |
    |---- 3. GLOBAL_COMMIT -------------->|                            |
    |---- 3. GLOBAL_COMMIT ------------------------------------------->|
    |                                     | [Releases Locks]           | [Releases Locks]
    |<--- 4. ACK -------------------------|                            |
    |<--- 4. ACK ------------------------------------------------------|

The Critical Flaw (Blocking State): If the Coordinator crashes immediately after receiving YES votes but before broadcasting GLOBAL_COMMIT, participants remain locked in the PREPARED state. They cannot release row locks, because they do not know whether the coordinator committed or aborted.

3.2 Saga Patterns: Orchestration vs. Choreography

A Saga is a sequence of local transactions with corresponding compensating transactions .

  • Choreography (Decentralized Events): Each service executes , commits locally, and emits a Kafka event. The next service listens to the event and executes . If failure occurs, compensating events are emitted backwards. Problem: Circular event dependencies and difficulty visualizing global workflow state.
  • Orchestration (Centralized State Machine): A dedicated orchestrator (e.g. Temporal, AWS Step Functions) coordinates the sequence, explicitly calling RPC endpoints for and invoking if any step fails.

4. Formal Mathematical Derivations

4.1 Latency Cost of 2PC vs. Local Commit

Let network round-trip time between coordinator and participants be . Let local disk fsync time for the transaction log be .

In a standard single-node transaction:

In a distributed 2PC transaction across participants in parallel:

Assuming and : Distributed 2PC introduces a end-to-end latency penalty compared to local commits while holding contended row locks across participants, capping per-key throughput via Little’s Law:

4.2 Availability Multiplication Penalty: Synchronous 2PC vs. Durable Sagas

In a synchronous 2PC transaction spanning independent microservice databases and 1 coordinator—each with independent steady-state availability —every participant and the coordinator must be simultaneously reachable for the transaction to commit:

  • Worked Example: Across services (Loyalty, Inventory, Payments, Shipping) plus 1 Coordinator, each boasting () uptime:
  • By contrast, an asynchronous Saga persisted in a replicated durable workflow log (Temporal, Apache Kafka) decouples temporal availability: order acceptance requires only the durable orchestrator (), while transient downstream service restarts are absorbed via exponential backoff retries or clean backward compensation.

5. Edge Cases, Failure Modes & Distributed Anomalies

5.1 The Semantic Anomaly of Sagas: Lack of Isolation

The fundamental compromise of the Saga pattern is that Sagas provide Atomicity, Consistency, and Durability, but NOT Isolation (ACD without I)! Because Step commits immediately to its local database:

  • Dirty Reads: Another user can read the intermediate state of before the Saga completes.
  • If Step subsequently fails and is executed to rollback , the external user has already acted upon data that was retroactively cancelled!

5.2 Out-of-Order Compensations

In an asynchronous network, a compensating transaction might arrive at a service before the forward transaction due to network routing delays! If the service executes (e.g. refund $50) on a non-existent account record, and (charge $50) arrives 2 seconds later, the user keeps the $50 charge and the refund is lost. Remediation: The service must record a “pre-cancellation tombstone” when arrives first, so that when eventually arrives, it is immediately rejected as a no-op.


6. Open-Source Architectural Studies

6.1 CockroachDB Parallel Commits & TiDB Async Commit over Multi-Raft

Distributed SQL databases eliminate the single-node 2PC coordinator bottleneck by storing both transaction status records and row-level lock intents inside Multi-Raft consensus groups:

  • CockroachDB Parallel Commits: The transaction coordinator writes provisional write intents to participant ranges and simultaneously writes a STAGING transaction record to the coordinator’s Raft group in parallel. Once both acknowledgments return to the client, the transaction is implicitly committed in , and the STAGING record is asynchronously promoted to COMMITTED.
  • TiDB / TiKV Percolator Async Commit & 1PC: TiKV builds on Google Percolator’s primary-lock 2PC model (designating one mutated key as the primary lock and pointing secondary locks to it). In TiDB 5.0+, 1PC commits single-Region transactions in one Raft round without 2PC, while Async Commit records all secondary keys on the primary lock during Prewrite so the client can be acknowledged as soon as all Prewrites succeed—cutting commit latency from to .

6.2 Temporal: Durable Execution Orchestrator

Temporal executes Sagas as deterministically replayable Go/TypeScript workflows. Every workflow step is recorded in a durable Cassandra/PostgreSQL execution history. If a worker process dies in the middle of executing a compensation, another worker rehydrates the workflow from the event log and resumes compensation from the exact step of failure.


7. Production Incident Post-Mortem (War Stories)

The 2PC Frozen Cluster Disaster

Context: A banking platform used an XA-compliant 2PC distributed transaction manager across Oracle and DB2 databases. The Incident: A high-voltage power surge in the primary data center killed the transaction coordinator server mid-transaction. The Impact: 85 high-volume customer accounts were left in the PREPARED state on the surviving Oracle database. The exclusive table locks blocked all subsequent deposit and withdrawal attempts for those accounts. Because the coordinator server’s local disk was fried, automated recovery could not determine if the transaction had committed or aborted. The Resolution: Database administrators spent 6 hours manually inspecting physical transaction logs and issuing manual COMMIT PREPARED and ROLLBACK PREPARED SQL statements. The Architectural Lesson: Never use blocking 2PC across independent microservices; replace synchronous locks with asynchronous Sagas or TCC resource reservations.


8. Production-Grade TypeScript Simulation: Distributed Saga Orchestrator

This simulation implements a complete state-machine Saga Orchestrator executing multi-step forward transactions with automatic, idempotent backward compensation on failure.

/**
 * Production Saga Orchestration Simulation with Compensating Workflows
 */

export interface SagaStep<TContext> {
  name: string;
  execute: (context: TContext) => Promise<void>;
  compensate: (context: TContext) => Promise<void>;
}

export enum SagaStatus {
  NOT_STARTED = "NOT_STARTED",
  IN_PROGRESS = "IN_PROGRESS",
  COMPLETED = "COMPLETED",
  COMPENSATING = "COMPENSATING",
  COMPENSATED = "COMPENSATED",
  FAILED = "FAILED",
}

export class SagaOrchestrator<TContext> {
  private steps: SagaStep<TContext>[] = [];
  public status: SagaStatus = SagaStatus.NOT_STARTED;
  public executionLog: string[] = [];

  public addStep(step: SagaStep<TContext>): this {
    this.steps.push(step);
    return this;
  }

  public async execute(context: TContext): Promise<boolean> {
    this.status = SagaStatus.IN_PROGRESS;
    const executedSteps: SagaStep<TContext>[] = [];

    for (let i = 0; i < this.steps.length; i++) {
      const step = this.steps[i];
      try {
        this.executionLog.push(`[EXECUTE] Step: ${step.name}`);
        await step.execute(context);
        executedSteps.push(step);
      } catch (err: unknown) {
        const message = err instanceof Error ? err.message : String(err);
        this.executionLog.push(`[FAILURE] Step: ${step.name} failed with: ${message}`);
        this.status = SagaStatus.COMPENSATING;
        await this.rollback(executedSteps, context);
        this.status = SagaStatus.COMPENSATED;
        return false;
      }
    }

    this.status = SagaStatus.COMPLETED;
    this.executionLog.push(`[SUCCESS] Saga completed successfully`);
    return true;
  }

  private async rollback(executedSteps: SagaStep<TContext>[], context: TContext): Promise<void> {
    // Compensate in reverse order
    for (let i = executedSteps.length - 1; i >= 0; i--) {
      const step = executedSteps[i];
      try {
        this.executionLog.push(`[COMPENSATE] Rolling back step: ${step.name}`);
        await step.compensate(context);
      } catch (compErr: unknown) {
        const message = compErr instanceof Error ? compErr.message : String(compErr);
        this.executionLog.push(
          `[CRITICAL] Compensation failed for ${step.name}: ${message}. Requires manual triage!`
        );
      }
    }
  }
}

// Verification Routine
export async function runSagaSimulation() {
  console.log("=== Distributed Saga Orchestration Simulation ===");

  interface OrderContext {
    orderId: string;
    userId: string;
    totalAmount: number;
    inventoryReserved: boolean;
    paymentCharged: boolean;
    shippingScheduled: boolean;
  }

  const context: OrderContext = {
    orderId: "ord_999",
    userId: "usr_42",
    totalAmount: 150,
    inventoryReserved: false,
    paymentCharged: false,
    shippingScheduled: false,
  };

  const saga = new SagaOrchestrator<OrderContext>();

  // Step 1: Inventory
  saga.addStep({
    name: "ReserveInventory",
    execute: async (ctx) => {
      ctx.inventoryReserved = true;
    },
    compensate: async (ctx) => {
      ctx.inventoryReserved = false;
    },
  });

  // Step 2: Payment
  saga.addStep({
    name: "ChargePayment",
    execute: async (ctx) => {
      ctx.paymentCharged = true;
    },
    compensate: async (ctx) => {
      ctx.paymentCharged = false;
    },
  });

  // Step 3: Shipping (Simulate intentional third-party failure)
  saga.addStep({
    name: "ScheduleShipping",
    execute: async () => {
      throw new Error("Carrier address verification rejected: invalid postal code");
    },
    compensate: async (ctx) => {
      ctx.shippingScheduled = false;
    },
  });

  const success = await saga.execute(context);
  console.log(`Saga Succeeded: ${success}`);
  console.log(`Final Saga Status: ${saga.status}`);
  console.log("Execution Trace:");
  for (const log of saga.executionLog) {
    console.log(`  ${log}`);
  }
  console.log("Final Context State:", context);
}

runSagaSimulation();

9. Operational Checklist, Failure Runbooks & Protocol Matrix

Distributed Transaction Comparison Matrix

PropertyTwo-Phase Commit (2PC)Three-Phase Commit (3PC)Saga OrchestrationTry-Confirm-Cancel (TCC)
ACID IsolationFull Serializable IsolationFull IsolationNone (Dirty Reads possible)Application-Level Isolation
Blocking on CrashYes (Coordinator crash halts nodes)No (assumes synchronous net)No (Asynchronous recovery)No (Timeout releases holds)
Throughput / LatencyLow / High Latency ()Low / Very High ()High / Low LatencyHigh / Moderate Latency
SuitabilitySharded SQL over Raft/PaxosTheoretical synchronous LANLong-running microservice flowsLow-latency financial reservations

10. Guided Applied System Design Exercises & Debugging Drills

Drill 1: Designing a Hotel Booking TCC Engine

Design a Try-Confirm-Cancel protocol for an online travel agency reserving rooms across 15 hotel partner APIs.

  1. Define the exact state machine transitions for the room entity in the partner’s database (AVAILABLE HELD_FOR_TRY CONFIRMED).
  2. What expiration mechanism ensures that a crashed client does not hold hotel rooms indefinitely?

Drill 2: Resolving Saga Dirty Read Anomalies

A user initiates a flight transfer. The Saga deducts $500 from Account A, and prepares to credit $500 to Account B. In the 2 seconds before Account B is credited, Account A’s mobile app queries its balance and displays the $500 deduction. A sudden network failure causes Account B’s credit to abort, and compensation refunds Account A.

  1. How do you design the user interface and database schema (e.g. pending balances) to prevent user panic over transient intermediate states?
View Reference Architecture & Worked Solutions (CORE-M22-001)

1. Worked Solution — Drill 1: Designing a Hotel Booking TCC Engine

  • Step 1 — Partner Room Entity State Machine & Idempotency Table:
    • Maintain an explicit room_reservations table alongside the inventory counter so Try, Confirm, and Cancel are strictly idempotent and safe against network reordering:
      CREATE TABLE room_inventory (
        hotel_id UUID,
        room_type TEXT,
        date DATE,
        available_count INT NOT NULL CHECK (available_count >= 0),
        held_count INT NOT NULL CHECK (held_count >= 0),
        confirmed_count INT NOT NULL CHECK (confirmed_count >= 0),
        PRIMARY KEY (hotel_id, room_type, date)
      );
      
      CREATE TABLE tcc_reservation_holds (
        tx_id UUID PRIMARY KEY,
        hotel_id UUID NOT NULL,
        state TEXT NOT NULL CHECK (state IN ('HELD_FOR_TRY', 'CONFIRMED', 'CANCELLED', 'EXPIRED')),
        expires_at TIMESTAMPTZ NOT NULL
      );
    • State Transitions:
      1. Try(tx_id, ttl=120s): In a single local ACID transaction, check if tx_id already exists in tcc_reservation_holds (if 'CANCELLED', reject as an out-of-order late Try). Otherwise decrement available_count = available_count - 1, increment held_count = held_count + 1, and insert ('HELD_FOR_TRY', NOW() + INTERVAL '120 seconds').
      2. Confirm(tx_id): If state is 'HELD_FOR_TRY', atomically transition to 'CONFIRMED', decrement held_count, and increment confirmed_count. (If already 'CONFIRMED', return 200 OK idempotently).
      3. Cancel(tx_id): If state is 'HELD_FOR_TRY', transition to 'CANCELLED', decrement held_count, and restore available_count = available_count + 1. If tx_id has not arrived yet, insert a pre-cancellation tombstone (state = 'CANCELLED') so a delayed Try packet cannot leak a hold.
  • Step 2 — Lease Expiration Reaper for Crashed Clients:
    • Every HELD_FOR_TRY row carries a bounded lease expires_at = NOW() + 120s. A background reaper (or lazy read-time check) scans WHERE state = 'HELD_FOR_TRY' AND expires_at < NOW() FOR UPDATE SKIP LOCKED, transitions expired holds to 'EXPIRED', and returns held_count back to available_count, guaranteeing non-blocking recovery if the travel agency orchestrator crashes.

2. Worked Solution — Drill 2: Resolving Saga Dirty Read Anomalies (available_balance vs. pending_debit)

  • Step 1 — Semantic Lock Schema Design (Dual-Balance Accounting):
    • Never overwrite a single balance column directly in Step 1 of an unisolated Saga. Instead, split the account state into settled_balance, pending_debit, and pending_credit, plus a transfer_status ledger:
    • Forward Step (Debit Account A $500):
      • Validates settled_balance - pending_debit >= 500, increments pending_debit += 500, and records ledger row (tx_id, status = 'IN_FLIGHT_OUTBOUND', amount = 500).
    • Forward Step (Credit Account B $500) + Finalization Step :
      • Once Account B confirms receipt, deducts settled_balance -= 500 and pending_debit -= 500 on Account A, marking the ledger row 'SETTLED'.
    • Compensating Step (If Account B Credit Aborts):
      • Simply decrements pending_debit -= 500 and updates the ledger row to 'REVERSED'—Account A’s settled_balance never falsely dropped!
  • Step 2 — UX Presentation to Prevent User Panic:
    • During the 2-second in-flight window, Account A’s mobile app displays: Settled Balance: $1,200.00 | Pending Transfer: -$500.00 (Processing...) | Available Balance: $700.00. If the transfer fails and compensates, the pending hold disappears cleanly without two jarring balance swings ($1,200 -> $700 -> $1,200) in the settled statement.

11. Summary & Architectural Decision Guide

  • Use Consensus-Backed 2PC (CockroachDB Parallel Commits, TiDB/TiKV Percolator, Google Spanner) when all participating shards belong to a single unified distributed SQL cluster where coordinators and participants are replicated Raft/Paxos groups rather than single points of failure.
  • Use Orchestrated Sagas (Temporal, AWS Step Functions) across autonomous microservices or third-party HTTP APIs (Stripe, FedEx) where holding cross-service locks is impossible; enforce idempotency keys and pre-cancellation tombstones on every forward and compensating handler.
  • Use Try-Confirm-Cancel (TCC) for high-contention financial wallets or inventory reservations where exposing intermediate uncommitted Saga deductions (dirty reads) violates business invariants.

12. Authoritative References & Version Context

MASTERY GATE · STEP 37 OF 94

Knowledge check & lesson completion

Score 4 / 4 (100%) on this check to mark this lesson Completed ✓, advance your position on the Master Spine, and add these concepts to your Spaced-Repetition Review Deck.

1. Why is the classic Two-Phase Commit (2PC) protocol classified as a 'blocking' atomic commitment protocol?
2. Why does the Three-Phase Commit (3PC) protocol—which avoids blocking under clean node crashes—fail to provide correctness in real-world distributed systems?
3. In a Saga distributed transaction architecture, how does the system maintain data consistency when Step 3 of a 5-step workflow fails?
4. How does the Try-Confirm-Cancel (TCC) pattern differ from a standard Saga?
PERSONAL NOTEBOOK

Chapter Notes

Personal engineering insights and observations saved to this browser.

0 notes
Tip: You can also highlight text in the article above to quote directly.

Loading personal notes...