---
title: Distributed Transactions
url: https://doc.liz6.com/en/distributed-systems/03-replication-and-consistency/03-distributed-transactions
locale: en
area: distributed-systems
tags:
- distributed-systems
- replication-and-consistency
date: 2026-06-30
modified: 2026-07-16
description: A write operation across multiple services—no global ACID. 2PC aims for strong consistency but blocks all participants if the coordinator fails; Saga and TCC trade availability for compensating transactions, at the cost of requiring application-level rollback logic. The outbox pattern and idempotency ensure the feasibility of "at-least-once" delivery in engineering practice.
---

# Distributed Transactions

> A write operation across multiple services—no global ACID. 2PC aims for strong consistency but blocks all participants if the coordinator fails; Saga and TCC trade availability for compensating transactions, at the cost of requiring application-level rollback logic. The outbox pattern and idempotency ensure the feasibility of "at-least-once" delivery in engineering practice.

In a single-node database, transactions are ACID—just use BEGIN/COMMIT/ROLLBACK. Distributed systems involve write operations across multiple services, with no global ACID transaction.

## 2PC (Two-Phase Commit)

The most basic distributed transaction protocol—blocking, with no fault tolerance for coordinator failure.

<svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="2PC Two-Phase Commit Process: PREPARE Voting Phase and COMMIT/ABORT Decision Phase">
  <defs><marker id="m2pc" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="320" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">2PC Two-Phase Commit: PREPARE Vote → COMMIT/ABORT Decision</text>

  <rect x="40" y="50" width="300" height="28" rx="6" fill="#4f46e5"/>
  <text x="190" y="69" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">① PREPARE Phase · Coordinator initiates voting</text>

  <rect x="52" y="90" width="276" height="44" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="109" font-size="12" font-weight="700" fill="#3730a3">Coordinator → Participants</text>
  <text x="66" y="126" font-size="11" fill="#4f46e5">Send PREPARE request</text>

  <rect x="52" y="140" width="276" height="60" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="159" font-size="12" font-weight="700" fill="#3730a3">Participant</text>
  <text x="66" y="176" font-size="11" fill="#4f46e5">Execute operation, write WAL, but do not commit</text>
  <text x="66" y="192" font-size="11" fill="#4f46e5">Reply YES (can commit) or NO</text>

  <rect x="52" y="206" width="276" height="32" rx="6" fill="#e0e7ff"/>
  <text x="190" y="227" text-anchor="middle" font-size="12" font-weight="600" fill="#3730a3">Collect all replies, determine if all are YES</text>

  <rect x="380" y="50" width="300" height="28" rx="6" fill="#0d9488"/>
  <text x="530" y="69" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">② Decision Phase · Coordinator broadcasts result</text>

  <rect x="392" y="90" width="276" height="54" rx="6" fill="#dcfce7" stroke="#4ade80"/>
  <text x="406" y="112" font-size="12" font-weight="700" fill="#166534">All YES → COMMIT</text>
  <text x="406" y="132" font-size="11" fill="#15803d">Coordinator broadcasts COMMIT, participants commit</text>

  <rect x="392" y="154" width="276" height="54" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="406" y="176" font-size="12" font-weight="700" fill="#9a3412">Any NO → ABORT</text>
  <text x="406" y="196" font-size="11" fill="#c2410c">Coordinator broadcasts ABORT, participants rollback</text>

  <line x1="330" y1="172" x2="388" y2="172" stroke="#475569" stroke-width="1.8" marker-end="url(#m2pc)"/>

  <rect x="40" y="248" width="640" height="56" rx="8" fill="#ffedd5"/>
  <text x="56" y="270" font-size="12.5" fill="#9a3412">Problem: If the coordinator crashes after PREPARE but before COMMIT, participants enter an uncertain state—</text>
  <text x="56" y="290" font-size="12.5" fill="#9a3412">They cannot unilaterally commit or abort, and must block waiting for the coordinator to recover. This is the blocking nature of the 2PC protocol.</text>
</svg>

Problem: If the coordinator crashes after PREPARE but before COMMIT → participants are in an uncertain state—they cannot unilaterally commit (they don't know if other participants replied YES) nor unilaterally abort (others might have already committed). **Participants block** until the coordinator recovers—this is the "blocking protocol".

In practice: XA transactions (an implementation of 2PC) are used for cross-shard database transactions, but handling coordinator crashes requires administrators to manually perform heuristic commit/abort.

## Saga

Breaks a long transaction into multiple local transaction steps, each with its own compensating action. If a step fails, the compensating actions for the completed steps are executed in reverse order.

<svg viewBox="0 0 720 330" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Saga Distributed Transaction: Four local transaction steps executed in order, with compensating actions executed in reverse order on failure">
  <defs><marker id="msaga" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="330" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Saga: book_trip() split into local transaction steps, compensating actions executed in reverse order on failure</text>

  <rect x="20" y="60" width="140" height="50" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="90" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">Step 1</text>
  <text x="90" y="98" text-anchor="middle" font-size="11" fill="#4f46e5">book_flight()</text>

  <rect x="200" y="60" width="140" height="50" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="270" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">Step 2</text>
  <text x="270" y="98" text-anchor="middle" font-size="11" fill="#4f46e5">book_hotel()</text>

  <rect x="380" y="60" width="140" height="50" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="450" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">Step 3</text>
  <text x="450" y="98" text-anchor="middle" font-size="11" fill="#4f46e5">book_car()</text>

  <rect x="560" y="60" width="140" height="50" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="630" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#9a3412">Step 4</text>
  <text x="630" y="98" text-anchor="middle" font-size="9.5" fill="#c2410c">process_payment() fails</text>

  <line x1="160" y1="85" x2="200" y2="85" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>
  <line x1="340" y1="85" x2="380" y2="85" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>
  <line x1="520" y1="85" x2="560" y2="85" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>

  <line x1="90" y1="110" x2="90" y2="138" stroke="#475569" stroke-width="1.4" marker-end="url(#msaga)"/>
  <line x1="270" y1="110" x2="270" y2="138" stroke="#475569" stroke-width="1.4" marker-end="url(#msaga)"/>
  <line x1="450" y1="110" x2="450" y2="138" stroke="#475569" stroke-width="1.4" marker-end="url(#msaga)"/>

  <rect x="20" y="140" width="140" height="40" rx="6" fill="#ffedd5"/>
  <text x="90" y="164" text-anchor="middle" font-size="10.5" font-weight="700" fill="#9a3412">Compensate: cancel_flight()</text>

  <rect x="200" y="140" width="140" height="40" rx="6" fill="#ffedd5"/>
  <text x="270" y="164" text-anchor="middle" font-size="10.5" font-weight="700" fill="#9a3412">Compensate: cancel_hotel()</text>

  <rect x="380" y="140" width="140" height="40" rx="6" fill="#ffedd5"/>
  <text x="450" y="164" text-anchor="middle" font-size="10.5" font-weight="700" fill="#9a3412">Compensate: cancel_car()</text>

  <text x="20" y="207" font-size="12" font-weight="700" fill="#1f2933">process_payment() fails → Compensating actions for completed steps executed in reverse order:</text>

  <rect x="20" y="216" width="200" height="34" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="120" y="238" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">process_payment() fails</text>

  <line x1="220" y1="233" x2="260" y2="233" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>

  <rect x="260" y="216" width="200" height="34" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="360" y="238" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">① cancel_hotel()</text>

  <line x1="460" y1="233" x2="500" y2="233" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>

  <rect x="500" y="216" width="200" height="34" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="600" y="238" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">② cancel_flight()</text>

  <rect x="20" y="258" width="680" height="56" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="36" y="280" font-size="12.5" fill="#3730a3">Saga lacks isolation—other transactions may see intermediate states, but it is non-blocking: the system is always progressing.</text>
  <text x="36" y="300" font-size="12.5" fill="#3730a3">Two orchestration patterns: Choreography (decentralized, event-driven) vs Orchestration (central coordinator, centralized logic, easier to manage).</text>
</svg>

Saga lacks isolation (other transactions can see intermediate states), but it is non-blocking—the system is always progressing at any moment.

### Orchestration Patterns

- **Choreography**: Each service consumes the event from the previous step and publishes its own completion/failure event—no central orchestrator.
- **Orchestration**: A central Saga coordinator tells each service to "execute step" or "execute compensation"—centralized logic, easier to manage.

## TCC (Try-Confirm-Cancel)

A variant of Saga, suitable for more "rigid" resource reservation:

```
Try:     Reserve resources (check inventory, freeze balance) — resources are **not consumed**, just marked as "pending use"
Confirm: Actually consume resources (deduct inventory, deduct funds)
Cancel:  Release reservation (release inventory, unfreeze balance)
```

Difference from Saga: Saga's steps directly modify data (book_flight actually books the ticket), and compensation is an undo. TCC's Try only reserves, and Confirm is the actual modification—**Cancel can be executed at any time without needing to perform an undo later**.

## Idempotency

The most important non-functional requirement in distributed systems: `f(x) = f(f(x))`—retrying an operation does not affect the result.

```
Implementation methods:
  1. Unique idempotency key: Client generates a UUID, server stores (key) → (processed/unprocessed)
     → Duplicate key → Return cached result, do not re-execute
  2. Database constraints: INSERT INTO orders VALUES (order_id=123, ...)
     → Retry → UNIQUE constraint violation → Ignore or read existing
  3. Token-based: Server gives client a token, client uses token to mark request
     → Duplicate token → Reject

Non-idempotent example:
  UPDATE users SET balance = balance + 100  -- Retry = added twice!
  Fix: UPDATE users SET balance = ? WHERE version = ? (optimistic locking)
```

## Outbox Pattern

Transactionally writing to the database and sending a message to the message queue—how to ensure atomicity between the two? You cannot write to DB first and then send the message (DB write succeeds but message sending fails → inconsistency).

```
[Local Transaction]:
  INSERT INTO orders (...);
  INSERT INTO outbox (event_type, payload, created_at) VALUES ('order_created', '...', NOW());
  COMMIT;  ← Both are in the same local transaction!

[Outbox Poller]:
  SELECT * FROM outbox ORDER BY id → Send to Kafka/MQ → On success → Delete from outbox
```

Guarantee: order write + event generation are atomic (same transaction). The Outbox poller ensures at-least-once delivery.

## References

- **Paper**: "Sagas" (Garcia-Molina & Salem, 1987)
- **Microservices Transactions**: microservices.io/patterns/data/saga.html
- **Idempotency**: stripe.com/docs/idempotency (production-grade implementation)

*Keywords: 2PC, XA, Saga, TCC, compensating action, idempotency, outbox pattern, distributed transaction*
