Distributed Transactions & Saga Pattern

📘 Chapter 9: Consistency & Distributed Consensus ⏱️ 10 min read 🏗️ Lesson 037

A single database transaction is straightforward — ACID guarantees handle it. But what happens when a business operation spans multiple services or databases? You can't just wrap everything in one transaction. This lesson covers two approaches: the classic Two-Phase Commit and the modern Saga pattern.

The Problem

Why Distributed Transactions Are Hard

Consider an e-commerce order:

  1. Order Service: Create order record
  2. Payment Service: Charge credit card
  3. Inventory Service: Reserve items
  4. Shipping Service: Schedule delivery

If payment succeeds but inventory is out of stock, you need to undo the payment. Each service has its own database — there's no single transaction that spans all of them.

Two-Phase Commit (2PC)

The classic approach: a coordinator orchestrates all participants through two phases.

How 2PC Works

  1. Phase 1 — Prepare: Coordinator asks each participant "Can you commit?" Each participant acquires locks, writes to a transaction log, and votes YES or NO.
  2. Phase 2 — Commit/Abort: If ALL vote YES → coordinator sends COMMIT to all. If ANY votes NO → coordinator sends ABORT to all.
Two-Phase Commit (2PC) Sequence Coordinator Participant A Participant B PREPARE Prepare? Vote: YES ✓ Vote: YES ✓ COMMIT All YES! COMMIT ACK ACK ⚠️ If coordinator crashes between Phase 1 & 2 → participants stuck holding locks!
Figure 1: 2PC — coordinator collects votes, then broadcasts commit or abort. Simple but blocking.

2PC Problems

  • Blocking: Participants hold locks while waiting for the coordinator's decision. If the coordinator dies, they're stuck.
  • Single Point of Failure: If the coordinator crashes after Phase 1 but before Phase 2, participants can't decide on their own.
  • Performance: Synchronous — all participants must be available and responsive. Latency = slowest participant.
  • Not partition-tolerant: If a network partition splits coordinator from participants, the system blocks.

The Saga Pattern

Instead of one distributed transaction, break it into a sequence of local transactions. Each step has a compensating action — an "undo" that runs if a later step fails.

Saga Pattern — Forward & Compensating Actions Forward → T1: Create Order T2: Charge Card T3: Reserve Inventory T4: Ship FAILS! ✗ ← Compensate trigger C3: Restock Items C2: Refund Card C1: Cancel Order Result: System back to consistent state — no partial execution persists ● Tn = Local transaction (forward) ● Cn = Compensating transaction (undo)
Figure 2: When step 4 fails, compensating actions undo steps 3, 2, and 1 in reverse order.

Choreography vs Orchestration

Choreography-Based Sagas

Each service listens for events and decides what to do next. No central coordinator.

  • Order Service publishes "OrderCreated" → Payment Service hears it, charges card, publishes "PaymentCharged"
  • Inventory Service hears "PaymentCharged" → reserves items, publishes "InventoryReserved"
  • If Payment fails → publishes "PaymentFailed" → Order Service hears it and cancels

Pros: Simple, loosely coupled, no SPOF. Cons: Hard to understand the full flow, difficult to debug, risk of cyclic dependencies.

Orchestration-Based Sagas

A central orchestrator (saga coordinator) tells each service what to do and handles the flow logic.

  • Orchestrator: "Payment Service, charge the card" → waits for response
  • Orchestrator: "Inventory Service, reserve items" → waits for response
  • If any step fails → orchestrator triggers compensations in reverse

Pros: Easy to understand, centralized flow logic, simpler testing. Cons: Orchestrator can be a bottleneck/SPOF, tighter coupling to coordinator.

Compensating Transactions

The "Undo" for Each Step

Compensations aren't simple rollbacks — they're new forward actions that semantically reverse the original:

  • Charged card → Compensation: Issue refund (not "undo the charge" — that's impossible once processed)
  • Reserved inventory → Compensation: Release reservation
  • Sent email → Compensation: Send cancellation email
  • Shipped package → Compensation: Initiate return process

Some actions are non-compensable (you can't un-send a physical package that's already delivered). Design sagas to put non-compensable steps last.

Real-World Examples

🏢 Airline Booking as a Saga

  1. Reserve seat (hold for 15 minutes) — Compensation: Release seat
  2. Charge credit card — Compensation: Refund payment
  3. Issue ticket — Compensation: Void ticket
  4. Send confirmation email — Compensation: Send cancellation email

If the credit card is declined at step 2, the system releases the seat (compensation for step 1). The customer's experience: "Sorry, payment failed. Please try again." No orphaned reservations.

🏢 Uber's Trip Lifecycle as a Saga

  • Match rider → driver — Compensation: Release driver back to pool
  • Start trip — Compensation: Cancel trip, no charge
  • Navigate & drive — (in progress, no compensation needed yet)
  • End trip & calculate fare — Compensation: Adjust fare
  • Charge payment — Compensation: If payment fails → flag for resolution, don't block the driver

Uber can't "undo" a completed ride, so the saga handles payment failures by flagging for later resolution rather than reversing the physical trip.

Interactive: Build a Saga

An e-commerce order saga is running. Click "Fail" on any step to trigger compensations: