Chapter 14: Distributed Transactions

2PC, 3PC, Saga, TCC, and Consistency Patterns

1. The Problem: ACID Across Multiple Nodes

Why Distributed Transactions Are Hard

Single-node transactions rely on ACID guarantees enforced by the database. When operations span multiple nodes, services, or databases, these guarantees break down.

The Fundamental Challenge

There is no global clock, no shared memory, and network failures are indistinguishable from slow responses. Any participant can fail at any time, and partial failures are the norm.

Failure Modes in Distributed Transactions

Failure Mode Description Impact Detection
Participant Crash A service goes down during transaction Uncertain state - did it commit? Timeout or explicit error
Coordinator Crash Central coordinator fails Participants left in limbo Timeout
Network Partition Network split isolates nodes Cannot reach consensus Timeout (indistinguishable from crash)
Message Loss Network drops packets Operations not received Timeout or missing ack
Partial Success Some participants commit, others fail Data inconsistency Explicit rollback needed
Byzantine Failure Node behaves maliciously or erratically Corrupted state propagation Verification, signatures

Motivating Example: E-Commerce Order

Placing an order requires coordinating multiple operations across services:

Order Placement Transaction:
1. Inventory Service: Reserve product (decrement stock)
2. Payment Service: Charge customer's card
3. Order Service: Create order record
4. Shipping Service: Create shipping request
5. Notification Service: Send confirmation email

What happens if:
- Step 2 fails? (Must unreserve inventory)
- Step 3 fails? (Must refund payment AND unreserve)
- Network partitions after step 2? (Money charged, but order not created)
sequenceDiagram participant C as Client participant I as Inventory participant P as Payment participant O as Order participant S as Shipping C->>I: Reserve item I-->>C: Reserved C->>P: Charge card P--xC: FAIL: Declined Note over C,I: Need to undo inventory reservation C->>I: Cancel reservation I-->>C: Released Note over C: Partial failure handled,
but what if we fail
AFTER charging?

The Dual-Write Problem

The classic anti-pattern that necessitates distributed transactions:

The Dual-Write Problem
// Anti-pattern: Two separate writes
database.insert(order);          // Write 1
messageQueue.publish(order);     // Write 2

// What if:
// - DB insert succeeds, but message publish fails?
// - Message publish succeeds, but DB insert fails?
// - Process crashes between the two operations?

// Result: Inconsistency between DB and downstream consumers

CAP Theorem Implications

Approach Consistency Availability Partition Tolerance Trade-off
2PC/3PC ✓ Strong ✗ Blocks ✗ Fails CP - unavailable during partition
Saga ~ Eventual ✓ High ✓ Handles AP - temporary inconsistency
TCC ✓ Strong ~ Medium ~ Partial Hybrid - reserves resources
Event Sourcing ~ Eventual ✓ High ✓ Handles AP - compensate via events
Interview Tip

When asked about distributed transactions, immediately identify the consistency requirements. Strong consistency (2PC) vs. eventual consistency (Saga) is the first decision point. Then discuss failure modes and recovery strategies.

2. Two-Phase Commit (2PC)

Overview

2PC is a blocking, synchronous protocol that ensures all participants either commit or abort. It uses a coordinator to orchestrate a two-phase process.

Protocol Steps

Phase Coordinator Action Participant Action Guarantees
Phase 1: Prepare Send PREPARE to all participants Vote YES (can commit) or NO (abort)
If YES: write to redo log, hold locks
After YES vote, participant MUST be able to commit
Phase 2: Commit If all YES: send COMMIT
If any NO: send ABORT
Execute COMMIT or ABORT
Release locks, respond with ACK
Decision is final and durable

Success Case: All Participants Commit

sequenceDiagram participant Co as Coordinator participant P1 as Participant 1 participant P2 as Participant 2 participant P3 as Participant 3 Note over Co: Phase 1: PREPARE Co->>P1: PREPARE Co->>P2: PREPARE Co->>P3: PREPARE Note over P1: Write redo log
Hold locks P1-->>Co: YES Note over P2: Write redo log
Hold locks P2-->>Co: YES Note over P3: Write redo log
Hold locks P3-->>Co: YES Note over Co: All YES received
Write COMMIT to log
Phase 2: COMMIT Co->>P1: COMMIT Co->>P2: COMMIT Co->>P3: COMMIT Note over P1: Apply changes
Release locks P1-->>Co: ACK Note over P2: Apply changes
Release locks P2-->>Co: ACK Note over P3: Apply changes
Release locks P3-->>Co: ACK Note over Co: Transaction complete

Failure Case: One Participant Votes NO

sequenceDiagram participant Co as Coordinator participant P1 as Participant 1 participant P2 as Participant 2 participant P3 as Participant 3 Note over Co: Phase 1: PREPARE Co->>P1: PREPARE Co->>P2: PREPARE Co->>P3: PREPARE P1-->>Co: YES P2-->>Co: NO (constraint violation) P3-->>Co: YES Note over Co: Received NO
Write ABORT to log
Phase 2: ABORT Co->>P1: ABORT Co->>P2: ABORT Co->>P3: ABORT Note over P1: Rollback
Release locks P1-->>Co: ACK Note over P2: Rollback
Release locks P2-->>Co: ACK Note over P3: Rollback
Release locks P3-->>Co: ACK Note over Co: Transaction aborted

The Blocking Problem

2PC's critical flaw: if the coordinator crashes after participants vote YES but before sending COMMIT, participants are stuck holding locks indefinitely.

sequenceDiagram participant Co as Coordinator participant P1 as Participant 1 participant P2 as Participant 2 Co->>P1: PREPARE Co->>P2: PREPARE P1-->>Co: YES (holding locks) P2-->>Co: YES (holding locks) Note over Co: CRASH!
(before writing COMMIT decision) Note over P1: Waiting...
Locks held
Cannot commit or abort Note over P2: Waiting...
Locks held
Cannot commit or abort Note over P1,P2: BLOCKED until coordinator recovers
The Blocking Problem

Participants cannot unilaterally decide to commit or abort after voting YES. They must wait for the coordinator. If the coordinator crashes, participants remain blocked, holding locks and preventing progress.

State Transitions

stateDiagram-v2 [*] --> Init state "Coordinator" as Coord { Init --> Preparing: Send PREPARE Preparing --> Committing: All YES Preparing --> Aborting: Any NO/Timeout Committing --> Committed: Send COMMIT Aborting --> Aborted: Send ABORT Committed --> [*] Aborted --> [*] } state "Participant" as Part { [*] --> Ready Ready --> Prepared: Vote YES Ready --> Abort: Vote NO Prepared --> Committed: Receive COMMIT Prepared --> Aborted: Receive ABORT Prepared --> BLOCKED: Coordinator crash Committed --> [*] Aborted --> [*] BLOCKED --> Committed: Coordinator recovers
with COMMIT BLOCKED --> Aborted: Coordinator recovers
with ABORT }

Implementation Details

2PC Coordinator Pseudocode
class TwoPhaseCommitCoordinator {
  function beginTransaction(participants) {
    txnId = generateId();
    log.write("BEGIN", txnId);

    // Phase 1: PREPARE
    votes = [];
    for (participant in participants) {
      vote = participant.prepare(txnId);
      votes.push(vote);

      if (vote == NO || timeout) {
        // Early abort
        decision = ABORT;
        break;
      }
    }

    // Make decision (write to durable log FIRST)
    if (all votes == YES) {
      decision = COMMIT;
    } else {
      decision = ABORT;
    }
    log.write(decision, txnId);  // CRITICAL: durable before sending

    // Phase 2: COMMIT or ABORT
    for (participant in participants) {
      participant.finalize(txnId, decision);
    }

    // Wait for acks (optional, fire-and-forget also works)
    waitForAcks(participants, txnId);
    log.write("END", txnId);

    return decision;
  }
}
2PC Participant Pseudocode
class TwoPhaseCommitParticipant {
  function prepare(txnId) {
    // Try to acquire locks and validate constraints
    if (!canCommit(txnId)) {
      return NO;
    }

    // Write tentative changes to redo log (durable)
    log.write("PREPARED", txnId, changes);

    // Hold locks until COMMIT or ABORT received
    locks.hold(txnId);

    return YES;  // Promise to commit if asked
  }

  function finalize(txnId, decision) {
    if (decision == COMMIT) {
      // Apply changes from redo log
      applyChanges(txnId);
      log.write("COMMITTED", txnId);
    } else {
      // Discard redo log
      rollback(txnId);
      log.write("ABORTED", txnId);
    }

    // Release locks
    locks.release(txnId);

    return ACK;
  }

  function recover(txnId) {
    // On restart, check log
    state = log.read(txnId);

    if (state == PREPARED) {
      // Still waiting for coordinator decision
      // Must contact coordinator or wait
      decision = askCoordinator(txnId);
      finalize(txnId, decision);
    }
  }
}

Recovery and Coordinator Failover

Scenario Coordinator State Participant State Recovery Action
Coordinator crashes before PREPARE No log entry No state Abort (transaction never started)
Coordinator crashes during PREPARE BEGIN logged, no decision Some YES, some nothing Abort (not all participants prepared)
Coordinator crashes after logging COMMIT COMMIT logged Some PREPARED, some COMMITTED Send COMMIT to remaining participants
Coordinator crashes after logging ABORT ABORT logged Some PREPARED, some ABORTED Send ABORT to remaining participants
Participant crashes after voting YES Waiting for acks PREPARED logged Participant asks coordinator on restart
Network partition during Phase 2 COMMIT logged Some unreachable Retry COMMIT until ack (or timeout and reconcile)

Performance Characteristics

Metric Value Explanation
Latency 2+ network round trips PREPARE + COMMIT phases, plus fsync for logs
Lock Duration Entire transaction + 2 RTTs Locks held from PREPARE to COMMIT ack
Throughput Low (blocking) Locks reduce concurrency, coordinator bottleneck
Availability Poor (coordinator SPOF) Single coordinator failure blocks all in-flight txns
Fault Tolerance Limited Cannot tolerate coordinator failure during commit phase

Real-World Usage

System Use Case Notes
XA Transactions JDBC, JTA (Java) Standard 2PC implementation for distributed DBs
MySQL Cluster Multi-node commits 2PC within cluster, not across clusters
PostgreSQL PREPARE TRANSACTION Manual 2PC support, rarely used in practice
MS DTC Windows distributed transactions Microsoft Distributed Transaction Coordinator
Oracle RAC Cluster coordination Modified 2PC with cache fusion
Interview Red Flags
  • Recommending 2PC for microservices (too brittle, high latency)
  • Not mentioning the blocking problem
  • Ignoring lock duration impact on throughput
  • Claiming 2PC is "highly available" (it's CP, not AP)

When to Use 2PC

  • Strong consistency is non-negotiable (financial transactions)
  • Participants are co-located (same data center, low latency)
  • Transaction volume is low (not a high-throughput system)
  • Participants support XA or similar protocol
  • Acceptable to block during coordinator failure (e.g., batch jobs)

When to Avoid 2PC

  • Microservices across multiple data centers (network latency kills performance)
  • High-throughput systems (lock duration bottleneck)
  • Need high availability during failures (coordinator SPOF)
  • Participants don't all support 2PC
  • Can tolerate eventual consistency (use Saga instead)

3. Three-Phase Commit (3PC)

Overview

3PC attempts to solve 2PC's blocking problem by adding a third phase: pre-commit. This allows participants to make progress even if the coordinator crashes, under certain assumptions.

The Three Phases

Phase Coordinator Action Participant Action Purpose
Phase 1: CanCommit Send CAN-COMMIT query Vote YES (can commit) or NO (abort) Same as 2PC PREPARE, but lighter
Phase 2: PreCommit If all YES: send PRE-COMMIT
If any NO: send ABORT
If PRE-COMMIT: ack and prepare to commit
If ABORT: abort
Participants know decision is coming
Phase 3: DoCommit Send DO-COMMIT Commit and release locks Actual commit

Success Case

sequenceDiagram participant Co as Coordinator participant P1 as Participant 1 participant P2 as Participant 2 Note over Co: Phase 1: CanCommit Co->>P1: CAN-COMMIT? Co->>P2: CAN-COMMIT? P1-->>Co: YES P2-->>Co: YES Note over Co: All YES
Phase 2: PreCommit Co->>P1: PRE-COMMIT Co->>P2: PRE-COMMIT Note over P1: Prepare to commit
Know commit is coming P1-->>Co: ACK Note over P2: Prepare to commit
Know commit is coming P2-->>Co: ACK Note over Co: Phase 3: DoCommit Co->>P1: DO-COMMIT Co->>P2: DO-COMMIT Note over P1: Commit
Release locks P1-->>Co: ACK Note over P2: Commit
Release locks P2-->>Co: ACK Note over Co: Transaction complete

Non-Blocking on Coordinator Failure

Key improvement: if coordinator crashes after PRE-COMMIT, participants know the decision was commit and can proceed.

sequenceDiagram participant Co as Coordinator participant P1 as Participant 1 participant P2 as Participant 2 Co->>P1: CAN-COMMIT? Co->>P2: CAN-COMMIT? P1-->>Co: YES P2-->>Co: YES Co->>P1: PRE-COMMIT Co->>P2: PRE-COMMIT P1-->>Co: ACK P2-->>Co: ACK Note over Co: CRASH!
(before DO-COMMIT) Note over P1: Timeout waiting for DO-COMMIT
But I know decision was commit Note over P1: After timeout, assume COMMIT P1->>P1: COMMIT Note over P2: Timeout waiting for DO-COMMIT
But I know decision was commit Note over P2: After timeout, assume COMMIT P2->>P2: COMMIT Note over P1,P2: NON-BLOCKING:
Can proceed without coordinator

State Transitions

stateDiagram-v2 [*] --> Init state "3PC Coordinator" as Co { Init --> CanCommit: Send CAN-COMMIT CanCommit --> PreCommitting: All YES CanCommit --> Aborting: Any NO/Timeout PreCommitting --> DoCommitting: Send PRE-COMMIT DoCommitting --> Committed: Send DO-COMMIT Aborting --> Aborted: Send ABORT Committed --> [*] Aborted --> [*] } state "3PC Participant" as Pa { [*] --> Wait Wait --> Prepared: Vote YES Wait --> Abort: Vote NO Prepared --> PreCommitted: Receive PRE-COMMIT Prepared --> Aborted: Receive ABORT/Timeout PreCommitted --> Committed: Receive DO-COMMIT PreCommitted --> Committed: Timeout (assume commit) Committed --> [*] Aborted --> [*] }

The Network Partition Problem

3PC's Achilles heel: it assumes a synchronous network with bounded delays. Network partitions break this assumption.

Why 3PC Fails Under Network Partition

If a network partition occurs after PRE-COMMIT is sent but before some participants receive it, the system splits into two groups:

  • Participants that received PRE-COMMIT assume COMMIT after timeout
  • Participants that didn't receive PRE-COMMIT assume ABORT after timeout
  • Result: Split-brain inconsistency
sequenceDiagram participant Co as Coordinator participant P1 as Participant 1 participant P2 as Participant 2 participant P3 as Participant 3 Co->>P1: CAN-COMMIT? Co->>P2: CAN-COMMIT? Co->>P3: CAN-COMMIT? P1-->>Co: YES P2-->>Co: YES P3-->>Co: YES Co->>P1: PRE-COMMIT Co->>P2: PRE-COMMIT Note over Co,P3: NETWORK PARTITION! Co-xP3: PRE-COMMIT (not delivered) P1-->>Co: ACK P2-->>Co: ACK Note over P1: Timeout, assume COMMIT P1->>P1: COMMIT Note over P2: Timeout, assume COMMIT P2->>P2: COMMIT Note over P3: Timeout in CanCommit phase
Never got PRE-COMMIT Note over P3: Assume ABORT (safe default) P3->>P3: ABORT Note over P1,P3: INCONSISTENCY!
P1,P2 committed
P3 aborted

Comparison: 2PC vs 3PC

Aspect 2PC 3PC
Phases 2 (PREPARE, COMMIT) 3 (CAN-COMMIT, PRE-COMMIT, DO-COMMIT)
Latency 2 round trips 3 round trips (50% higher)
Blocking on Coordinator Crash YES (after PREPARE) NO (after PRE-COMMIT, participants assume commit)
Network Partition Handling Blocks but stays consistent Can split-brain (P1 commits, P2 aborts)
Consistency Guarantee Strong (all or nothing) Strong only in synchronous networks
Real-World Usage Common (XA, databases) Rare (theory only, too fragile)
Failure Model Fail-stop (crash-recovery) Assumes synchronous network (unrealistic)

Why 3PC is Rarely Used

  • Higher Latency: 50% more round trips than 2PC
  • Network Partition Vulnerability: Can lead to split-brain inconsistency
  • Synchronous Network Assumption: Real networks are asynchronous
  • Timeout Complexity: Choosing correct timeout values is hard
  • No Significant Benefit: In practice, coordinator crashes are rare enough that 2PC blocking is acceptable, or eventual consistency (Saga) is better
Interview Tip

When discussing 3PC, emphasize that it's a theoretical improvement over 2PC but fails under network partitions. In practice, 2PC is preferred for strong consistency (accepting the blocking risk), or Saga for eventual consistency (avoiding blocking entirely).

Theoretical Interest vs. Practical Reality

Aspect Theory Practice
Non-blocking Participants can proceed after timeout Only safe in synchronous networks (unrealistic)
Fault Tolerance Survives coordinator failure Only if no network partition occurs
Adoption Promising improvement over 2PC Almost never used (too complex, too fragile)
Partition Tolerance Assumed not to happen (synchronous model) Partitions are common in real networks

Conclusion: 3PC is a textbook protocol that demonstrates the difficulty of achieving non-blocking distributed consensus. In practice, systems use 2PC for strong consistency (accepting blocking) or move to eventual consistency patterns (Saga, TCC) that embrace asynchrony.

4. Saga Pattern

Overview

Saga is an eventual consistency pattern that breaks a distributed transaction into a sequence of local transactions, each with a compensating transaction to undo it if needed. No distributed locks, no blocking.

Key Insight

Instead of locking resources across services (2PC), let each service commit locally, then compensate if something fails later. Trade strong consistency for availability and scalability.

Two Flavors: Choreography vs. Orchestration

Aspect Choreography Orchestration
Coordination Decentralized - services react to events Centralized - orchestrator controls flow
Communication Event-driven (pub/sub) Request/response (RPC)
Complexity Distributed (harder to debug) Localized (easier to reason about)
Coupling Services must know next step Services are decoupled (orchestrator knows flow)
Visibility Harder to track saga state Orchestrator has full visibility
Best For Simple workflows, loose coupling Complex workflows, need observability

Choreography: Event-Driven Saga

Each service publishes events, and other services react by executing their local transaction and publishing the next event.

sequenceDiagram participant O as Order Service participant I as Inventory Service participant P as Payment Service participant S as Shipping Service participant Q as Event Bus Note over O: 1. Create order O->>O: Create order (PENDING) O->>Q: OrderCreated event Note over I: 2. Reserve inventory Q->>I: OrderCreated event I->>I: Reserve stock I->>Q: InventoryReserved event Note over P: 3. Charge payment Q->>P: InventoryReserved event P->>P: Charge card P->>Q: PaymentCharged event Note over S: 4. Create shipment Q->>S: PaymentCharged event S->>S: Create shipping request S->>Q: ShipmentCreated event Note over O: 5. Complete order Q->>O: ShipmentCreated event O->>O: Update order (COMPLETED) O->>Q: OrderCompleted event

Choreography: Failure and Compensation

sequenceDiagram participant O as Order Service participant I as Inventory Service participant P as Payment Service participant Q as Event Bus O->>O: Create order (PENDING) O->>Q: OrderCreated event Q->>I: OrderCreated event I->>I: Reserve stock I->>Q: InventoryReserved event Q->>P: InventoryReserved event P->>P: Charge card Note over P: FAIL: Card declined P->>Q: PaymentFailed event Note over I: Compensate: Unreserve Q->>I: PaymentFailed event I->>I: Release stock I->>Q: InventoryReleased event Note over O: Mark order failed Q->>O: InventoryReleased event O->>O: Update order (FAILED) O->>Q: OrderFailed event

Orchestration: Centralized Saga Coordinator

A dedicated orchestrator service controls the flow, calling each participant and handling failures.

sequenceDiagram participant C as Client participant Orch as Saga Orchestrator participant I as Inventory Service participant P as Payment Service participant S as Shipping Service participant O as Order Service C->>Orch: Create order Note over Orch: Step 1: Reserve inventory Orch->>I: Reserve stock I-->>Orch: Reserved Note over Orch: Step 2: Charge payment Orch->>P: Charge card P-->>Orch: Charged Note over Orch: Step 3: Create shipment Orch->>S: Create shipping request S-->>Orch: Created Note over Orch: Step 4: Finalize order Orch->>O: Create order record O-->>Orch: Created Orch-->>C: Order completed

Orchestration: Failure and Rollback

sequenceDiagram participant C as Client participant Orch as Saga Orchestrator participant I as Inventory Service participant P as Payment Service participant S as Shipping Service C->>Orch: Create order Orch->>I: Reserve stock I-->>Orch: Reserved ✓ Note over Orch: Save state: inventory reserved Orch->>P: Charge card P-->>Orch: Charged ✓ Note over Orch: Save state: payment charged Orch->>S: Create shipping request S--xOrch: FAIL: No carrier available Note over Orch: ROLLBACK: Execute compensating transactions Orch->>P: Refund payment P-->>Orch: Refunded Orch->>I: Release stock I-->>Orch: Released Orch-->>C: Order failed (reason: shipping unavailable)

Compensating Transactions

Compensating transactions are NOT rollbacks - they are forward-correcting operations that semantically undo the effect.

Forward Transaction Compensating Transaction Notes
Reserve inventory Release reservation Idempotent: safe to call multiple times
Charge payment Issue refund May require manual review if disputes occur
Send email Send cancellation email Cannot "unsend" original email
Create shipping label Cancel shipment May incur cancellation fee
Allocate budget Deallocate budget Must handle partial consumption
Update user profile Revert to previous profile Need to store old state
Compensating Transaction Design

Not all actions are compensable:

  • Cannot compensate: Sending an email, publishing to external API, physical world actions
  • Partial compensation: Refunds may have fees, shipment cancellation may have costs
  • Time-sensitive: Compensation may fail if too much time passes (e.g., shipment already delivered)

Saga State Machine

stateDiagram-v2 [*] --> Step1: Start Saga Step1 --> Step2: Success Step1 --> Compensate1: Failure Step2 --> Step3: Success Step2 --> Compensate2: Failure Step3 --> Step4: Success Step3 --> Compensate3: Failure Step4 --> Completed: Success Step4 --> Compensate4: Failure Compensate4 --> Compensate3: Compensated Compensate3 --> Compensate2: Compensated Compensate2 --> Compensate1: Compensated Compensate1 --> Failed: All compensated Completed --> [*] Failed --> [*] note right of Compensate1: Compensating transactions execute in REVERSE order

Implementation: Orchestrator Pseudocode

Saga Orchestrator
class SagaOrchestrator {
  function executeSaga(sagaDefinition, input) {
    sagaId = generateId();
    state = initializeState(sagaId, input);

    try {
      // Execute forward steps
      for (step in sagaDefinition.steps) {
        result = executeStep(step, state);

        if (result.failed) {
          throw new SagaFailure(step, result.error);
        }

        // Save state after each step (for recovery)
        state = updateState(sagaId, step, result);
      }

      // All steps succeeded
      markCompleted(sagaId);
      return state;

    } catch (error) {
      // Saga failed - execute compensating transactions
      compensate(sagaId, state, error);
      throw error;
    }
  }

  function compensate(sagaId, state, originalError) {
    completedSteps = state.getCompletedSteps();

    // Execute compensations in REVERSE order
    for (step in reverse(completedSteps)) {
      try {
        compensatingTxn = step.getCompensation();
        compensatingTxn.execute(state);

        state = updateState(sagaId, "compensated_" + step.name, null);

      } catch (compensationError) {
        // Compensation failed - log and retry
        log.error("Compensation failed for step " + step.name, compensationError);
        retryCompensation(sagaId, step, state);
      }
    }

    markFailed(sagaId, originalError);
  }

  function recover(sagaId) {
    // On restart, load state from durable storage
    state = loadState(sagaId);

    if (state.status == "IN_PROGRESS") {
      // Resume saga from last completed step
      resumeSaga(sagaId, state);
    } else if (state.status == "COMPENSATING") {
      // Resume compensation
      resumeCompensation(sagaId, state);
    }
  }
}

Saga Execution Guarantees (SEC)

Property Guarantee Enforcement
Semantic Atomicity Either all steps complete OR all compensations execute Orchestrator must retry compensations on failure
Eventual Consistency System reaches consistent state eventually Compensations must succeed (retries + manual intervention)
Isolation NOT guaranteed (dirty reads possible) Use semantic locks or versioning if needed
Lack of Isolation

Saga's biggest weakness: intermediate states are visible. If Order Saga reserves inventory but payment fails, another client might see "low stock" during the compensation window. Mitigations:

  • Semantic locks: Mark inventory as "pending" vs "reserved"
  • Commutative operations: Design operations to be order-independent
  • Versioning: Read-only transactions see consistent snapshots
  • Accept anomalies: Document that temporary inconsistencies are expected

Real-World Example: Order Placement Saga

Step Service Forward Transaction Compensating Transaction
1 Inventory Reserve items (set status=RESERVED) Release reservation (set status=AVAILABLE)
2 Payment Authorize card (hold funds) Cancel authorization (release funds)
3 Payment Capture payment (charge card) Refund payment (return funds)
4 Loyalty Allocate points (credit account) Revoke points (debit account)
5 Shipping Create shipment (generate label) Cancel shipment (void label)
6 Order Create order record (status=PLACED) Mark order cancelled (status=CANCELLED)
7 Notification Send confirmation email Send cancellation email

Saga Orchestration Tools

Tool Type Features Best For
Temporal Workflow engine Durable execution, automatic retries, versioning Complex workflows, long-running sagas
Cadence Workflow engine Similar to Temporal (predecessor) Uber's open-source version
Camunda BPMN workflow Visual workflow designer, BPMN standard Enterprise workflows, compliance
AWS Step Functions Managed orchestration Serverless, visual workflow, retry policies AWS-native applications
Apache Airflow DAG-based workflow Python-based, batch-oriented Data pipelines, batch jobs
Netflix Conductor Microservice orchestrator JSON workflow definition, polyglot Microservice coordination
Eventuate Tram Saga Saga framework Java library, event-driven Spring Boot microservices

When to Use Saga

  • Distributed transactions across microservices
  • Can tolerate eventual consistency (not immediate consistency)
  • Each step can be compensated (or manual intervention is acceptable)
  • Need high availability and throughput (no blocking)
  • Long-running transactions (minutes to hours)

When to Avoid Saga

  • Strong consistency required (use 2PC or single database)
  • Steps cannot be compensated (e.g., physical actions)
  • Isolation is critical (intermediate states must not be visible)
  • Simple use case (overhead not justified)
Interview Tip

When asked about microservice transactions, Saga is usually the right answer. Emphasize orchestration for complex workflows (better observability) and choreography for simple event-driven flows. Always discuss compensating transactions and lack of isolation.

Complete Saga Example: E-Commerce Order (All 5 Steps + All Compensations)

This end-to-end example demonstrates a complete order placement saga, including the exact sequence of forward transactions and compensating transactions when a step fails.

Forward Transactions (Success Path)

Step Service Action Result
1 Order Service createOrder(items, user_id) order_id=ORD-123
status=PENDING
2 Inventory Service reserveItems(order_id, items) reservation_id=RES-456
Stock marked RESERVED
3 Payment Service chargeCard(order_id, amount=$99.50, idempotency_key=ORD-123-PAY) payment_id=PAY-789
Card charged
4 Shipping Service createShipment(order_id, address) shipment_id=SHIP-012
Label generated
5 Notification Service sendConfirmation(order_id, user_email) Email queued for delivery

Failure Scenario: Shipping Service Down at Step 4

Problem: After successfully charging the customer's card (step 3), the shipping service API is down. The saga must compensate by refunding the payment, releasing inventory, and canceling the order.

Compensating Transactions (Executed in Reverse Order)

Step Service Compensation Action Result
5 Notification Service Never executed (saga failed before this step) No compensation needed
4 Shipping Service Failed (API down) ❌ Trigger compensation chain
3 Payment Service refundPayment(payment_id=PAY-789, reason="shipping_failed") refund_id=REF-345
$99.50 refunded to card
2 Inventory Service releaseReservation(reservation_id=RES-456) Stock status restored to AVAILABLE
1 Order Service cancelOrder(order_id=ORD-123, reason="shipping_unavailable") status=CANCELLED
User notified

Key Saga Rules

Critical Requirements for Compensation
  1. Reverse Order: Compensations execute in reverse order (5 → 4 → 3 → 2 → 1) to maintain consistency
  2. Idempotency: Each compensation must be idempotent (safe to retry). If refundPayment(PAY-789) is called twice, the second call is a no-op
  3. Eventual Success: Compensations must EVENTUALLY succeed. If releaseReservation fails (Inventory service down), retry forever with exponential backoff
  4. Dead Letter Queue: After 10 retries (or configurable threshold), move failed compensation to DLQ for human intervention
  5. State Persistence: Saga state (completed steps, pending compensations) must be durably stored (database, workflow engine) to survive coordinator crashes

Saga Flow: Success and Failure Paths

sequenceDiagram participant Orch as Saga Orchestrator participant Order as Order Service participant Inv as Inventory Service participant Pay as Payment Service participant Ship as Shipping Service participant Notif as Notification Service Note over Orch: Start Saga: Order Placement Orch->>Order: 1. createOrder(items, user_id) Order-->>Orch: ✓ order_id=ORD-123, status=PENDING Orch->>Inv: 2. reserveItems(ORD-123, items) Inv-->>Orch: ✓ reservation_id=RES-456 Orch->>Pay: 3. chargeCard(ORD-123, $99.50) Pay-->>Orch: ✓ payment_id=PAY-789 Note over Orch,Ship: Step 4 fails - Shipping API down Orch->>Ship: 4. createShipment(ORD-123, address) Ship-->>Orch: ❌ Error: Service unavailable (503) Note over Orch: Saga Failed at Step 4
Execute Compensations (Reverse Order) Note over Orch,Notif: Step 5 never executed - skip compensation Orch->>Pay: COMPENSATE 3: refundPayment(PAY-789, reason="shipping_failed") Pay-->>Orch: ✓ refund_id=REF-345 Orch->>Inv: COMPENSATE 2: releaseReservation(RES-456) Inv-->>Orch: ✓ Stock restored to AVAILABLE Orch->>Order: COMPENSATE 1: cancelOrder(ORD-123, reason="shipping_unavailable") Order-->>Orch: ✓ status=CANCELLED Note over Orch: Saga Compensated Successfully
Customer refunded, inventory released

Alternative: Success Path

sequenceDiagram participant Orch as Saga Orchestrator participant Order as Order Service participant Inv as Inventory Service participant Pay as Payment Service participant Ship as Shipping Service participant Notif as Notification Service Note over Orch: Start Saga: Order Placement Orch->>Order: 1. createOrder(items, user_id) Order-->>Orch: ✓ order_id=ORD-123, status=PENDING Orch->>Inv: 2. reserveItems(ORD-123, items) Inv-->>Orch: ✓ reservation_id=RES-456 Orch->>Pay: 3. chargeCard(ORD-123, $99.50) Pay-->>Orch: ✓ payment_id=PAY-789 Orch->>Ship: 4. createShipment(ORD-123, address) Ship-->>Orch: ✓ shipment_id=SHIP-012, label generated Orch->>Notif: 5. sendConfirmation(ORD-123, user@example.com) Notif-->>Orch: ✓ Email queued Note over Orch: All Steps Completed
Saga Succeeded Orch->>Order: updateOrderStatus(ORD-123, status=CONFIRMED) Order-->>Orch: ✓ Order confirmed

Handling Compensation Failures: Retry and DLQ

What if a compensation fails? Example: Payment service refunds the card successfully, but Inventory service is down and cannot release the reservation.

Compensation Retry Logic:
1. Attempt releaseReservation(RES-456)
2. If fails: Retry after 1s (exponential backoff: 1s, 2s, 4s, 8s, ...)
3. After 10 retries (or 5 minutes elapsed): Move to Dead Letter Queue
4. Alert operations team: "Saga ORD-123 compensation stuck: inventory reservation RES-456 not released"
5. Human intervention: Manually release reservation or wait for Inventory service recovery and retry
Why Compensations Must Eventually Succeed

If compensation fails permanently, the system is in an inconsistent state: customer refunded but inventory still marked RESERVED. This is why saga compensations must be designed to eventually succeed (either by fixing the service or manual intervention). Unlike forward transactions (which can fail and trigger compensation), compensations cannot fail permanently.

Idempotency Example: Refund Payment

Saga orchestrator might call refundPayment(PAY-789) multiple times due to retries or crashes. The payment service must ensure idempotency:

function refundPayment(payment_id, reason) {
  // Check if refund already exists (idempotency)
  existing_refund = db.query("SELECT * FROM refunds WHERE payment_id = ?", payment_id);
  if (existing_refund) {
    return existing_refund; // Already refunded, return existing refund
  }

  // First time - execute refund
  refund_id = stripe.refund(payment_id, reason);
  db.insert("INSERT INTO refunds (refund_id, payment_id, reason, created_at) VALUES (?, ?, ?, NOW())", refund_id, payment_id, reason);

  return { refund_id: refund_id, status: "success" };
}

Result: Calling refundPayment(PAY-789) 10 times results in exactly 1 refund, not 10 refunds.

Real-World Production Details

Aspect Implementation
State Storage PostgreSQL table: saga_instances (saga_id, current_step, status, created_at, updated_at)
saga_step_log (saga_id, step_num, service, action, result, timestamp)
Retry Policy Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s (max)
Max retries: 10 attempts or 5 minutes total
Timeout Each step: 30s timeout
Entire saga: 5 minutes timeout (prevents infinite hangs)
Observability Metrics: saga_started_total, saga_completed_total, saga_failed_total, saga_compensation_total
Traces: Each saga is a single trace with spans for each step
Logs: Structured logs with saga_id, step, service, action, result
Dead Letter Queue SQS/Kafka topic: saga-compensation-dlq
Alert on message arrival → PagerDuty escalation
Idempotency Keys Format: {saga_id}-{step_num}-{service}
Example: ORD-123-3-payment (ensures step 3 payment is executed exactly once)
When to Show This Level of Detail in Interviews

Start with the high-level saga flow (forward transactions → failure → compensations in reverse). If the interviewer asks "How do you ensure compensations succeed?" or "What if a compensation fails?", dive into retry logic, idempotency, and DLQ. This demonstrates production-readiness, not just theoretical knowledge.

5. TCC Pattern (Try-Confirm-Cancel)

Overview

TCC is a three-phase distributed transaction pattern that reserves resources upfront (Try), then either commits (Confirm) or releases (Cancel) them. It's a middle ground between 2PC and Saga.

The Three Phases

Phase Purpose Participant Action Guarantee
Try Reserve resources Lock or mark resources as "pending"
Validate constraints
Return success/failure
If Try succeeds, resources are reserved and Confirm WILL succeed
Confirm Commit the transaction Execute business logic with reserved resources
Release locks
Must be idempotent (may be retried)
Cancel Abort and release resources Release reserved resources
Rollback any Try phase changes
Must be idempotent (may be retried)

TCC Flow: Success Case

sequenceDiagram participant Co as Coordinator participant I as Inventory Service participant P as Payment Service participant O as Order Service Note over Co: Phase 1: TRY Co->>I: Try: Reserve stock Note over I: Mark stock as RESERVED
Create reservation record I-->>Co: Success (reservation ID) Co->>P: Try: Hold funds Note over P: Authorize card
Create hold record P-->>Co: Success (hold ID) Co->>O: Try: Create order Note over O: Create order with status=PENDING
Store order ID O-->>Co: Success (order ID) Note over Co: All Try succeeded
Phase 2: CONFIRM Co->>I: Confirm: Commit reservation Note over I: Change RESERVED → SOLD
Delete reservation record I-->>Co: Confirmed Co->>P: Confirm: Capture payment Note over P: Capture held funds
Delete hold record P-->>Co: Confirmed Co->>O: Confirm: Finalize order Note over O: Change PENDING → CONFIRMED O-->>Co: Confirmed Note over Co: Transaction complete

TCC Flow: Failure Case (Cancel)

sequenceDiagram participant Co as Coordinator participant I as Inventory Service participant P as Payment Service participant O as Order Service Note over Co: Phase 1: TRY Co->>I: Try: Reserve stock I-->>Co: Success (reservation ID) Co->>P: Try: Hold funds Note over P: Card authorization FAILED P--xCo: Failure (insufficient funds) Note over Co: Try phase failed
Phase 3: CANCEL Co->>I: Cancel: Release reservation Note over I: Delete reservation
Mark stock as AVAILABLE I-->>Co: Cancelled Note over Co: Transaction cancelled

How TCC Differs from Saga

Aspect Saga TCC
Resource Reservation No - commits immediately Yes - reserves in Try phase
Isolation None (dirty reads possible) Better (reserved resources not visible)
Consistency Window Eventual (compensation takes time) Shorter (Try → Confirm is fast)
Complexity Moderate (2 operations per service) High (3 operations per service)
Performance Fast (each service commits independently) Slower (extra Try phase)
Use Case General microservice transactions Financial systems, inventory mgmt

TCC Implementation: Inventory Service Example

TCC Inventory Service
class InventoryService {
  // Try: Reserve stock
  function tryReserve(productId, quantity, txnId) {
    // Check if enough stock available
    available = db.query("SELECT quantity FROM inventory WHERE product_id = ?", productId);

    if (available < quantity) {
      return { success: false, error: "Insufficient stock" };
    }

    // Create reservation record (tentative hold)
    reservation = {
      id: generateId(),
      txnId: txnId,
      productId: productId,
      quantity: quantity,
      status: "RESERVED",
      expiresAt: now() + 10minutes  // Timeout for Try phase
    };

    db.insert("reservations", reservation);

    // Decrement available inventory (but don't commit sale yet)
    db.update("inventory SET reserved = reserved + ? WHERE product_id = ?", quantity, productId);

    return { success: true, reservationId: reservation.id };
  }

  // Confirm: Commit the reservation
  function confirm(txnId) {
    reservation = db.query("SELECT * FROM reservations WHERE txn_id = ?", txnId);

    if (!reservation) {
      // Already confirmed or cancelled - idempotent
      return { success: true };
    }

    // Move from reserved to sold
    db.update("inventory SET sold = sold + ?, reserved = reserved - ? WHERE product_id = ?",
              reservation.quantity, reservation.quantity, reservation.productId);

    // Delete reservation record
    db.delete("reservations WHERE txn_id = ?", txnId);

    return { success: true };
  }

  // Cancel: Release the reservation
  function cancel(txnId) {
    reservation = db.query("SELECT * FROM reservations WHERE txn_id = ?", txnId);

    if (!reservation) {
      // Already cancelled - idempotent
      return { success: true };
    }

    // Release reserved inventory
    db.update("inventory SET reserved = reserved - ? WHERE product_id = ?",
              reservation.quantity, reservation.productId);

    // Delete reservation record
    db.delete("reservations WHERE txn_id = ?", txnId);

    return { success: true };
  }

  // Background job: Clean up expired reservations
  function cleanupExpiredReservations() {
    expired = db.query("SELECT * FROM reservations WHERE expires_at < ?", now());

    for (reservation in expired) {
      cancel(reservation.txnId);
    }
  }
}

TCC Coordinator

TCC Coordinator
class TCCCoordinator {
  function executeTransaction(participants, txnId) {
    tryResults = [];

    try {
      // Phase 1: TRY on all participants
      for (participant in participants) {
        result = participant.try(txnId);

        if (!result.success) {
          throw new TryFailure(participant, result.error);
        }

        tryResults.push({ participant, result });
      }

      // All Try succeeded - log COMMIT decision
      log.write("TCC_COMMIT", txnId);

      // Phase 2: CONFIRM on all participants
      for (entry in tryResults) {
        entry.participant.confirm(txnId);
      }

      log.write("TCC_COMPLETED", txnId);
      return { success: true };

    } catch (error) {
      // Try phase failed - log CANCEL decision
      log.write("TCC_CANCEL", txnId, error);

      // Phase 3: CANCEL on all participants (including ones that succeeded)
      for (entry in tryResults) {
        try {
          entry.participant.cancel(txnId);
        } catch (cancelError) {
          // Cancel must succeed - retry
          retryCancel(entry.participant, txnId);
        }
      }

      log.write("TCC_CANCELLED", txnId);
      return { success: false, error };
    }
  }

  function recover(txnId) {
    state = log.read(txnId);

    if (state == "TCC_COMMIT") {
      // Coordinator decided COMMIT - resume Confirm phase
      resumeConfirm(txnId);
    } else if (state == "TCC_CANCEL") {
      // Coordinator decided CANCEL - resume Cancel phase
      resumeCancel(txnId);
    }
  }
}

Idempotency Requirements

All TCC operations (Try, Confirm, Cancel) MUST be idempotent because the coordinator may retry them.

Operation Idempotency Strategy Example
Try Check if reservation already exists for txnId If reservation exists, return existing reservation ID
Confirm Check if already confirmed (reservation record deleted) If no reservation found, assume already confirmed, return success
Cancel Check if already cancelled (reservation record deleted) If no reservation found, assume already cancelled, return success

TCC State Diagram

stateDiagram-v2 [*] --> Initial Initial --> Trying: Start TCC Trying --> TrySucceeded: All Try operations succeed Trying --> TryFailed: Any Try operation fails TrySucceeded --> Confirming: Coordinator decides COMMIT Confirming --> Confirmed: All Confirm operations complete TryFailed --> Cancelling: Coordinator decides CANCEL Cancelling --> Cancelled: All Cancel operations complete Confirmed --> [*] Cancelled --> [*] note right of TrySucceeded: Resources are reserved, Must Confirm or Cancel note right of Confirming: Confirm must succeed, Retry on failure note right of Cancelling: Cancel must succeed, Retry on failure

Real-World Use Cases

Domain Use Case Try Confirm Cancel
E-Commerce Order placement Reserve inventory, authorize payment Commit sale, capture payment Release inventory, void authorization
Banking Money transfer Hold funds in source account Debit source, credit destination Release hold
Travel Booking Flight + hotel Reserve flight seat, reserve hotel room Book flight, book hotel Release seat, release room
Cloud Resources Multi-resource allocation Reserve VM, reserve storage, reserve IP Provision VM, provision storage, assign IP Release reservations
Ticket Sales Concert tickets Hold tickets for 10 minutes Sell tickets Release held tickets

Advantages of TCC

  • Better Isolation: Reserved resources not visible to other transactions
  • Predictable: If Try succeeds, Confirm will succeed (no surprises)
  • Shorter Inconsistency Window: Faster than Saga's eventual consistency
  • No Rollback Complexity: Cancel is simpler than Saga compensation

Disadvantages of TCC

  • High Complexity: Every service must implement 3 operations (Try, Confirm, Cancel)
  • Resource Locking: Reservations hold resources (availability impact)
  • Timeout Management: Must handle expired Try phase reservations
  • Extra Round Trip: Try phase adds latency
  • Idempotency Required: All operations must be idempotent (implementation burden)

When to Use TCC

  • Financial transactions where isolation matters (e.g., prevent double-spending)
  • Inventory management with strict availability guarantees
  • Resource allocation systems (cloud, tickets, reservations)
  • Need better isolation than Saga but can't afford 2PC blocking

When to Avoid TCC

  • Services don't naturally support reservations (implementation burden too high)
  • Long-running transactions (holding reservations for hours is costly)
  • Isolation not critical (Saga is simpler)
  • Need true ACID guarantees (use 2PC or single database)
Interview Tip

TCC is the "smart candidate" answer for financial systems and inventory problems. It shows you understand the trade-off between Saga (eventual consistency, no isolation) and 2PC (strong consistency, blocking). Emphasize the reservation mechanism and idempotency requirements.

9. Comparison Matrix

Overview Table: All Patterns

Pattern Consistency Availability Latency Complexity Isolation Scalability
2PC ✅ Strong ❌ Low (blocks) ❌ High (2 RTTs + locks) 🟡 Medium ✅ Full (locks) ❌ Low (coordinator bottleneck)
3PC 🟡 Strong* (partition breaks) 🟡 Medium ❌ Very High (3 RTTs) ❌ High ✅ Full (locks) ❌ Low
Saga 🟡 Eventual ✅ High ✅ Low (parallel commits) 🟡 Medium ❌ None ✅ High
TCC ✅ Strong (with reservations) 🟡 Medium 🟡 Medium (3 phases) ❌ High (3 ops/service) 🟡 Partial (reserved state) 🟡 Medium
Outbox ✅ Atomic (DB + event) ✅ High 🟡 Medium (relay delay) 🟡 Medium N/A (not a transaction protocol) ✅ High

Failure Handling Comparison

Pattern Coordinator Crash Participant Crash Network Partition Recovery Complexity
2PC ❌ Blocks participants 🟡 Coordinator waits (timeout) ❌ Blocks or aborts 🟡 Medium (coordinator log)
3PC 🟡 Non-blocking* (if no partition) 🟡 Timeout and proceed ❌ Can split-brain ❌ High (complex state machine)
Saga ✅ Orchestrator recovers from log ✅ Retry or compensate ✅ Eventually consistent 🟡 Medium (idempotent steps)
TCC ✅ Recovers from decision log ✅ Retry Confirm/Cancel 🟡 Timeout reservations ❌ High (timeout management)
Outbox ✅ Relay restarts and resumes ✅ Event persisted in DB ✅ Tolerant (eventual) ✅ Low (simple retry)

Use Case Fit

Scenario Best Pattern Why Avoid
Financial transactions (strong consistency required) 2PC or TCC Need atomicity, isolation critical Saga (no isolation, eventual)
Microservices (independent teams, loosely coupled) Saga (choreography) Decentralized, event-driven, scales well 2PC (coupling, blocking)
E-commerce order (cart → payment → shipping) Saga (orchestration) Complex workflow, need observability 3PC (unnecessary complexity)
Inventory reservation (prevent overbooking) TCC Need isolation via reservations Saga (dirty reads of inventory)
Event-driven architecture (DB + publish event) Outbox Atomic DB + event, no dual-write Direct publish (message loss risk)
Single datacenter, low latency, XA support 2PC Network reliable, strong consistency Saga (eventual consistency overkill)
Multi-region, high throughput, eventual consistency OK Saga + Outbox Scalable, partition-tolerant 2PC (latency, blocking)
Payment processing (no double-charge) TCC or 2PC + Idempotency keys Isolation critical, idempotent retries Saga without semantic locks

Decision Tree

graph TD A[Distributed Transaction Needed] --> B{Strong consistency required?} B -->|Yes| C{Same datacenter?} B -->|No| D{Need isolation?} C -->|Yes| E[2PC
XA Transactions] C -->|No| F[TCC
Reservations] D -->|Yes| G[TCC
Semantic Locks] D -->|No| H{Complex workflow?} H -->|Yes| I[Saga
Orchestration] H -->|No| J[Saga
Choreography] style E fill:#FFC107 style F fill:#FF9800 style G fill:#FF9800 style I fill:#4CAF50 style J fill:#4CAF50

Performance Characteristics

Pattern Throughput P99 Latency Lock Duration Network Hops Coordinator Load
2PC Low (blocking) High (100-500ms) Entire transaction 2 per participant High (all txns)
3PC Very Low Very High (150-750ms) Entire transaction 3 per participant Very High
Saga High (no blocking) Low (10-100ms per step) None (local commits) 1 per step Low (orchestrator only tracks state)
TCC Medium Medium (50-200ms) Try → Confirm window 3 per participant Medium
Outbox High (async) Low (DB write latency) None (event publish async) 0 (async relay) None (relay is separate)

Implementation Complexity

Pattern Service Changes Infrastructure Testing Complexity Debugging
2PC Low (XA support) Transaction manager (DTC, JTA) Low (synchronous) Easy (coordinator log)
3PC High (custom impl) Custom coordinator Very High (timeouts, partitions) Hard (distributed state)
Saga High (compensations) Orchestrator (Temporal, Conductor) High (async, eventual) Medium (workflow visibility)
TCC Very High (3 ops each) Coordinator + timeout mgmt Very High (try/confirm/cancel) Hard (reservation leaks)
Outbox Medium (outbox table) Relay or CDC (Debezium) Medium (idempotency) Easy (outbox table inspection)
Interview Strategy

Start with clarifying questions:

  • What consistency level is required? (Strong → 2PC/TCC, Eventual → Saga)
  • What's the failure tolerance? (Network partition → avoid 2PC)
  • What's the scale? (High throughput → Saga, Low → 2PC OK)
  • Do services support XA? (Yes → 2PC option, No → Saga)

Then recommend pattern with trade-offs, don't just pick one blindly.

10. Real-World Patterns

Uber: Trip Lifecycle with Saga

Architecture

graph TB Client[Rider App] --> Gateway[API Gateway] Gateway --> Trip[Trip Service] Trip --> Cadence[Cadence Workflow] Cadence --> Rider[Rider Service] Cadence --> Driver[Driver Service] Cadence --> Payment[Payment Service] Cadence --> Map[Map Service] Cadence --> Notification[Notification Service] Trip --> Kafka[Kafka Event Bus] Kafka --> Analytics[Analytics] Kafka --> Billing[Billing] style Cadence fill:#FF9800 style Kafka fill:#4CAF50

Trip Request Saga (Orchestrated with Cadence)

Step Service Action Compensation Timeout
1 Rider Service Validate rider (credit check) N/A (read-only) 5s
2 Map Service Calculate route, estimate fare N/A (read-only) 10s
3 Payment Service Pre-authorize card (hold funds) Cancel authorization 30s
4 Driver Service Find and assign driver Unassign driver 60s
5 Trip Service Create trip record (ACTIVE) Cancel trip 5s
6 Notification Service Send push to rider & driver Send cancellation push 10s

Why Cadence?

  • Durable Execution: Workflow state persisted, survives failures
  • Automatic Retries: Failed steps retry with exponential backoff
  • Timeout Management: Each activity has timeout, workflow can compensate
  • Versioning: Can deploy new workflow versions without breaking in-flight trips
  • Observability: Full visibility into workflow execution (Cadence UI)

Stripe: Payment Idempotency and Webhooks

Architecture

graph LR Client[Merchant Server] -->|Idempotency-Key| API[Stripe API] API --> DB[(PostgreSQL)] API --> Queue[Event Queue] Queue --> Webhook[Webhook Delivery Service] Webhook -->|Retry with backoff| MerchantA[Merchant A] Webhook -->|Retry with backoff| MerchantB[Merchant B] DB --> Outbox[Outbox Table] Outbox --> Queue style API fill:#6772E5 style Queue fill:#FF9800

Idempotency Key Flow

Stripe Payment Request
POST https://api.stripe.com/v1/charges
Headers:
  Authorization: Bearer sk_test_...
  Idempotency-Key: 7f3d9c8b-4e2a-4c9f-8e1a-5b6d7c8e9f0a
Body:
  amount=1000
  currency=usd
  source=tok_visa
  description=Order #12345

// Stripe's handling:
1. Check if Idempotency-Key exists in `idempotency_keys` table
2. If exists and status=completed → return cached response
3. If exists and status=processing → return 409 Conflict
4. If new → process payment, cache result with key
5. Return response

// Client can safely retry on network failure
// Same Idempotency-Key = same result (no double charge)

Webhook Delivery (Outbox Pattern)

Step Description Guarantees
1. Payment Processing Stripe processes payment, writes to DB ACID transaction
2. Event Creation Insert event into outbox table (same txn) Atomic with payment
3. Event Publishing Background worker publishes from outbox At-least-once delivery
4. Webhook Delivery POST to merchant's webhook URL Retry with exponential backoff (up to 3 days)
5. Idempotent Handling Merchant checks event ID (deduplication) Exactly-once semantics (at merchant)
Stripe Webhook Event
{
  "id": "evt_1J3Q4R2eZvKYlo2C0X5Y6Z7A",  // Event ID for deduplication
  "type": "charge.succeeded",
  "created": 1625097600,
  "data": {
    "object": {
      "id": "ch_1J3Q4R2eZvKYlo2C0X5Y6Z7A",
      "amount": 1000,
      "currency": "usd",
      "status": "succeeded"
    }
  }
}

// Merchant's webhook handler (idempotent):
function handleWebhook(event) {
  // Check if already processed
  exists = db.query("SELECT 1 FROM processed_stripe_events WHERE event_id = ?", event.id);
  if (exists) {
    return { status: 200, message: "Already processed" };
  }

  // Process event
  db.beginTransaction();
  updateOrder(event.data.object.id);
  db.insert("processed_stripe_events", { event_id: event.id });
  db.commit();

  return { status: 200 };
}

DoorDash: Order Fulfillment Saga

Architecture

graph TB Client[Customer App] --> Order[Order Service] Order --> Temporal[Temporal Workflow] Temporal --> Restaurant[Restaurant Service] Temporal --> Dasher[Dasher Dispatch] Temporal --> Payment[Payment Service] Temporal --> Notification[Notification Service] Restaurant --> Menu[Menu Service] Dasher --> Location[Location Service] Order --> EventBus[Event Bus] EventBus --> Analytics[Analytics] EventBus --> Fraud[Fraud Detection] style Temporal fill:#2196F3 style EventBus fill:#4CAF50

Order Placement Saga

Step Service Action Compensation SLA
1 Payment Pre-authorize card Cancel authorization 5s
2 Restaurant Submit order to restaurant (POS) Cancel order at restaurant 30s
3 Dasher Dispatch Find and assign Dasher Unassign Dasher 10min
4 Payment Capture payment (charge card) Refund payment 10s
5 Notification Send confirmation (SMS, push) Send cancellation notice 5s

Temporal Workflow Code (Simplified)

Order Fulfillment Workflow
@WorkflowMethod
public void fulfillOrder(Order order) {
  try {
    // Step 1: Pre-authorize payment
    PaymentResult preAuth = activities.preAuthorizePayment(order.paymentMethod, order.total);
    workflow.sleep(Duration.ofSeconds(1)); // Wait for auth to settle

    // Step 2: Submit to restaurant
    RestaurantResult restaurant = activities.submitToRestaurant(order.restaurantId, order.items);

    // Step 3: Assign Dasher (with timeout)
    DasherResult dasher = activities.assignDasher(
      order.restaurantLocation,
      order.deliveryLocation,
      Duration.ofMinutes(10)
    );

    // Step 4: Capture payment
    activities.capturePayment(preAuth.authId);

    // Step 5: Send notifications
    activities.sendNotification(order.customerId, "Order confirmed!");
    activities.sendNotification(dasher.dasherId, "New delivery!");

    // Mark workflow complete
    activities.completeOrder(order.id);

  } catch (Exception e) {
    // Compensate in reverse order
    compensate(order, e);
  }
}

private void compensate(Order order, Exception originalError) {
  try {
    // Cancel in reverse order
    activities.cancelDasherAssignment(order.id);
    activities.cancelRestaurantOrder(order.id);
    activities.cancelPaymentAuth(order.id);
    activities.sendNotification(order.customerId, "Order cancelled: " + originalError.getMessage());
    activities.markOrderFailed(order.id, originalError);

  } catch (Exception compensationError) {
    // Log and alert - manual intervention needed
    log.error("Compensation failed for order " + order.id, compensationError);
    alertOncall("Compensation failure", order.id, compensationError);
  }
}

Why Temporal?

  • Long-Running Workflows: Dasher assignment can take 10+ minutes
  • Automatic Retries: Activity failures retry automatically
  • Durable Timers: workflow.sleep() survives process restarts
  • Versioning: Can change workflow logic without breaking in-flight orders
  • Visibility: Temporal UI shows all in-flight orders, stuck workflows

Netflix: Event Sourcing for User Activity

Architecture

graph LR Client[Netflix App] --> API[API Service] API --> Kafka[Kafka Event Log] Kafka --> ViewHistory[View History Service] Kafka --> Recommendations[Recommendation Engine] Kafka --> Analytics[Analytics Pipeline] Kafka --> BillingEvents[Billing Events] ViewHistory --> Cassandra[(Cassandra)] Recommendations --> ElasticSearch[(ElasticSearch)] style Kafka fill:#FF9800 style Cassandra fill:#4CAF50 style ElasticSearch fill:#2196F3

Event Sourcing Pattern

All state changes are stored as immutable events. Current state is derived by replaying events.

Event Type Payload Consumers
VideoStarted { userId, videoId, timestamp } View history, Recommendations, Analytics
VideoProgress { userId, videoId, position, timestamp } Resume playback, Watch time tracking
VideoCompleted { userId, videoId, timestamp } Recommendations, Completion rate
VideoRated { userId, videoId, rating, timestamp } Recommendations, Content quality
ProfileCreated { userId, profileId, name } User service, Billing

Guarantees

  • Immutable Event Log: Events never deleted (Kafka retention = forever for critical topics)
  • Ordering Per User: Kafka partition key = userId (all user events in order)
  • Replay Capability: Can rebuild any view by replaying events
  • Schema Evolution: Use Avro with schema registry (backward/forward compatible)
  • Idempotent Consumers: All consumers handle duplicate events (event ID deduplication)
Interview Tip

When discussing real-world systems, mention specific tools (Temporal, Cadence, Kafka) and patterns (Outbox, Event Sourcing). Show you understand trade-offs: Uber uses Cadence for observability, Stripe uses Outbox for webhook reliability, Netflix uses Event Sourcing for replayability.

11. When to Avoid Distributed Transactions

Alternative: Eventual Consistency

Many problems that seem to require distributed transactions can be solved with eventual consistency and domain modeling.

Example: Bank Transfer

Approach Implementation Trade-offs
2PC (Strong Consistency) Lock both accounts, debit source, credit destination, commit ✅ Atomic
❌ Blocks both accounts
❌ Low throughput
Saga (Eventual Consistency) Debit source → emit event → credit destination
Compensate if destination fails
✅ High throughput
❌ Temporary inconsistency
🟡 Need compensating transaction
Event Sourcing (Eventual Consistency) Log "TransferInitiated" event → async processors update accounts ✅ Audit trail
✅ Replayable
❌ Complexity
❌ Eventual consistency

Alternative: Single Database

If possible, keep related data in the same database. Use microservices when needed, but don't split data unnecessarily.

Majestic Monolith

A well-designed monolith with a single database is simpler, faster, and more consistent than distributed microservices. Only split when you have clear scaling or organizational reasons (team autonomy, independent deployment).

When to Keep Data Together

  • Strong consistency required: Financial transactions, inventory management
  • Frequent joins: Queries span multiple entities (splitting causes N+1 problem)
  • Low scale: Single DB can handle load (splitting adds complexity without benefit)
  • ACID guarantees: Need rollback, foreign keys, constraints

Alternative: Accepting Inconsistency

Some business domains naturally tolerate inconsistency. Document and communicate these trade-offs.

Examples of Acceptable Inconsistency

Domain Inconsistency Mitigation
E-Commerce Inventory Overselling (more orders than stock) Apologize, refund, offer discount for next purchase
Social Media Likes Like count slightly off (cached) Accept approximation (exact count not critical)
Analytics Dashboards Metrics delayed by minutes Label as "Last updated 5 min ago"
Recommendations Recommendations stale (not reflecting latest views) Accept staleness (recommendations are probabilistic anyway)
Leaderboards Rankings update with delay Accept eventual consistency (users refresh to see latest)

Alternative: Reconciliation Jobs

Instead of preventing inconsistency, detect and fix it after the fact.

Reconciliation Pattern

Daily Reconciliation Job
// Run nightly to detect and fix inconsistencies
function reconcileOrders() {
  // Find orders that are PAID but not SHIPPED after 24 hours
  stuckOrders = db.query(`
    SELECT o.id, o.status, p.status as payment_status, s.status as shipping_status
    FROM orders o
    JOIN payments p ON o.id = p.order_id
    LEFT JOIN shipments s ON o.id = s.order_id
    WHERE o.status = 'PAID'
      AND (s.status IS NULL OR s.status = 'PENDING')
      AND o.created_at < NOW() - INTERVAL 24 HOUR
  `);

  for (order in stuckOrders) {
    if (order.payment_status == 'COMPLETED' && order.shipping_status == NULL) {
      // Payment succeeded but shipment never created - create it now
      log.warn("Reconciliation: Creating missing shipment for order", order.id);
      shippingService.createShipment(order.id);

    } else if (order.payment_status == 'FAILED' && order.shipping_status != NULL) {
      // Payment failed but shipment exists - cancel shipment
      log.warn("Reconciliation: Canceling shipment for failed payment", order.id);
      shippingService.cancelShipment(order.id);
      orderService.updateStatus(order.id, 'CANCELLED');
    }
  }
}

Reconciliation Use Cases

  • Financial Systems: Daily balance reconciliation (sum of transactions = account balance)
  • Inventory: Physical count vs. system count (adjust discrepancies)
  • Distributed Caches: Compare cache vs. DB, invalidate stale entries
  • Multi-Region: Compare data across regions, flag differences

Alternative: CQRS (Command Query Responsibility Segregation)

Separate write model (commands) from read model (queries). Accept that read model is eventually consistent.

graph LR Client[Client] -->|Write| Command[Command Handler] Command --> WriteDB[(Write DB)] Command --> EventBus[Event Bus] EventBus --> Projector[Projector] Projector --> ReadDB1[(Read DB 1
Customer View)] Projector --> ReadDB2[(Read DB 2
Reporting)] Projector --> ReadDB3[(Read DB 3
Search)] Client -->|Read| Query[Query Handler] Query --> ReadDB1 Query --> ReadDB2 Query --> ReadDB3 style Command fill:#f44336 style Query fill:#4CAF50 style EventBus fill:#FF9800
Aspect Write Side Read Side
Consistency Strong (single write DB) Eventual (async projection)
Model Normalized, enforces business rules Denormalized, optimized for queries
Scalability Limited (single write DB) High (multiple read DBs)
Technology PostgreSQL, MySQL (relational) ElasticSearch, MongoDB, Redis (optimized for reads)

Decision Framework: Do I Need Distributed Transactions?

graph TD A[Multiple Services Need to Coordinate] --> B{Can I use a single database?} B -->|Yes| C[Use Single DB
ACID guarantees
Simple] B -->|No| D{Strong consistency required?} D -->|Yes| E{Same datacenter?} D -->|No| F[Use Saga
Eventual consistency
High availability] E -->|Yes| G[Use 2PC
Accept blocking
XA transactions] E -->|No| H[Use TCC
Reservations
Accept complexity] D --> I{Can I accept temporary inconsistency?} I -->|Yes| J[Use Saga + Reconciliation
Fix inconsistencies later] I -->|No| K[Reconsider architecture
Maybe split is wrong] style C fill:#4CAF50 style F fill:#2196F3 style G fill:#FFC107 style H fill:#FF9800 style J fill:#4CAF50 style K fill:#f44336

Anti-Patterns to Avoid

Anti-Pattern Problem Alternative
Distributed Transactions Everywhere Every microservice operation requires 2PC Consolidate related data into same service/DB
Saga for Simple Workflows 3-step process doesn't need Saga complexity Use single database transaction
2PC Across WAN High latency, network partition risk Use Saga or TCC (eventual consistency)
No Idempotency Retries cause duplicate side effects Always design for idempotent operations
Ignoring Compensation Failures Saga compensation fails, stuck in inconsistent state Implement retry + alerting + manual intervention
No Observability Cannot debug distributed transactions Use orchestrator (Temporal) or distributed tracing

The KISS Principle

Keep It Simple, Stupid. Distributed transactions add immense complexity. Avoid them if possible.

Complexity Budget

Every distributed transaction pattern (2PC, Saga, TCC) consumes your team's complexity budget. Use it wisely:

  • Start with a monolith and single database (simple, fast, consistent)
  • Split only when you have clear scaling or organizational reasons
  • When you split, accept eventual consistency where possible (Saga)
  • Use strong consistency (2PC/TCC) only for critical business requirements
  • Invest in observability (Temporal, distributed tracing) to manage complexity
Interview Closer

End your answer by acknowledging trade-offs: "I'd start with Saga for eventual consistency and high availability. If business requires strong consistency, I'd consider TCC for isolation or 2PC for same-datacenter deployments. But first, I'd question if we can avoid distributed transactions entirely by consolidating related data."