What it is
A distributed transaction is any operation that must modify data across multiple services or data stores and leave them in a consistent state — either all succeed, or the effect of partial success is undone. The problem is that networks fail, nodes crash, and there is no global lock in a distributed system. These three patterns — 2PC, 3PC, and saga — each trade different things to achieve coordination.
When you care
Distributed transactions appear the moment a system design question involves two or more services that must agree on an outcome: placing an order (payment + inventory), transferring money between accounts, or reserving a seat on two connecting flights. The interviewer is checking whether you understand that “just use a transaction” stops working across service boundaries, and what the real options are — with their real costs.
The three patterns at a glance
| Pattern | Coordination | Consistency | Blocking | Compensation | Typical use |
|---|---|---|---|---|---|
| 2PC | Central coordinator | Strong (atomic) | Yes — participants block on coordinator | Not needed — either all commit or all abort | Single-owner databases, XA transactions |
| 3PC | Central coordinator | Strong (non-blocking in theory) | Reduced — pre-commit phase allows timeout recovery | Not needed | Rarely used in practice |
| Saga | Orchestrator or events | Eventual | No — each step completes independently | Required — every step needs a compensating action | Microservices, long-lived workflows |
Two-Phase Commit (2PC)
What it is
A protocol where a coordinator asks all participants (databases, services) to prepare, then tells them to commit or abort based on unanimous agreement. The name comes from the two phases: prepare (vote) and commit/abort (decision).
The sequence
Phase 1 — Prepare (voting)
┌─────────────────────────────────────────────────────┐
│ Coordinator Participant A Participant B │
│ │ │ │ │
│ │──── PREPARE ──────────►│ │ │
│ │──── PREPARE ──────────────────────────►│ │
│ │ │ │ │
│ │◄─── VOTE YES ──────────│ │ │
│ │◄─── VOTE YES ──────────────────────────│ │
└─────────────────────────────────────────────────────┘
Phase 2 — Commit (decision)
┌─────────────────────────────────────────────────────┐
│ Coordinator Participant A Participant B │
│ │ │ │ │
│ │──── COMMIT ───────────►│ │ │
│ │──── COMMIT ───────────────────────────►│ │
│ │ │ │ │
│ │◄─── ACK ───────────────│ │ │
│ │◄─── ACK ───────────────────────────────│ │
└─────────────────────────────────────────────────────┘
Step by step:
- Coordinator sends PREPARE to all participants.
- Each participant writes a prepare record to its local write-ahead log (WAL), acquires locks on the affected rows, and replies VOTE YES if it can commit, VOTE NO if it cannot.
- If all votes are YES, coordinator writes a COMMIT decision to its own log, then sends COMMIT to all participants.
- If any vote is NO (or a timeout), coordinator writes ABORT, sends ABORT to all.
- Participants apply the commit or abort, release locks, and ACK.
Why it works
The protocol guarantees atomicity: no participant can commit unless all participants have promised they can (by voting YES). The coordinator’s decision record is durable — after a crash, recovery replays it and re-sends the decision.
Why it’s dangerous
The blocking problem: between voting YES and receiving the COMMIT decision, a participant is blocked. It holds locks, cannot timeout safely (because it doesn’t know the coordinator’s decision), and must wait. If the coordinator crashes after collecting votes but before broadcasting the decision:
- All participants are stuck. They voted YES, they hold locks, they cannot abort unilaterally (another participant might have committed). They must wait for the coordinator to recover.
- No participant can safely proceed. This is the fundamental limitation of 2PC — the coordinator is a single point of failure during the critical window between vote and decision.
This isn’t theoretical. In practice, coordinator crashes during the commit window cause:
- Lock escalation storms on the participants’ databases.
- Cascade failures if other transactions queue behind the blocked locks.
- Manual intervention to resolve “in-doubt” transactions.
When to use 2PC
- Within a single database cluster. Postgres, MySQL InnoDB, and Oracle internally use 2PC-like protocols for distributed writes across shards. The coordinator is co-located and highly available, which mitigates the blocking risk.
- XA transactions. When two databases (say, Postgres and an MQ broker) need to atomically agree. Supported by JDBC/JTA in Java. Works if the set of participants is small and local.
- Short-lived, low-latency operations. The blocking window is proportional to the operation duration. A 5ms prepare + 5ms commit is acceptable; a 500ms prepare + 30s external API call is not.
When NOT to use 2PC
- Across microservices with independent databases. The blocking window is too long, the coordinator is too far, and services must remain autonomous.
- Across external services you don’t control (payment gateways, third-party APIs). They won’t implement the 2PC participant protocol.
- Long-lived workflows (order processing, booking flows). Holding locks for minutes is catastrophic.
Three-Phase Commit (3PC)
What it is
An extension of 2PC that adds a pre-commit phase between voting and committing, designed to eliminate the blocking problem. If the coordinator crashes, participants can use timeouts to recover without waiting.
The sequence
Phase 1 — Can-commit (voting)
┌─────────────────────────────────────────────────────┐
│ Coordinator Participant A Participant B │
│ │──── CAN-COMMIT ──────►│ │ │
│ │──── CAN-COMMIT ──────────────────────►│ │
│ │◄─── YES ──────────────│ │ │
│ │◄─── YES ──────────────────────────────│ │
└─────────────────────────────────────────────────────┘
Phase 2 — Pre-commit
┌─────────────────────────────────────────────────────┐
│ Coordinator Participant A Participant B │
│ │──── PRE-COMMIT ──────►│ │ │
│ │──── PRE-COMMIT ──────────────────────►│ │
│ │◄─── ACK ──────────────│ │ │
│ │◄─── ACK ──────────────────────────────│ │
└─────────────────────────────────────────────────────┘
Phase 3 — Do-commit
┌─────────────────────────────────────────────────────┐
│ Coordinator Participant A Participant B │
│ │──── DO-COMMIT ───────►│ │ │
│ │──── DO-COMMIT ───────────────────────►│ │
│ │◄─── DONE ─────────────│ │ │
│ │◄─── DONE ─────────────────────────────│ │
└─────────────────────────────────────────────────────┘
Step by step:
- Can-commit. Coordinator asks “can you commit?” Participants reply YES or NO. Same as 2PC phase 1 but no locks acquired yet.
- Pre-commit. If all voted YES, coordinator sends PRE-COMMIT. Participants acquire locks, write to WAL, and ACK. This is the new phase — it tells participants that everyone has voted YES.
- Do-commit. Coordinator sends DO-COMMIT. Participants apply the change and release locks.
How it solves the blocking problem
The key insight: after receiving PRE-COMMIT, a participant knows that all participants voted YES. If the coordinator now crashes:
- A participant that received PRE-COMMIT can timeout and commit unilaterally — it knows everyone agreed.
- A participant that never received PRE-COMMIT (still in phase 1) can timeout and abort — it knows the decision was never finalized.
This eliminates the “stuck holding locks forever” failure mode of 2PC.
Why 3PC is rarely used in practice
The theoretical non-blocking property breaks under network partitions. If the network splits such that some participants received PRE-COMMIT and others didn’t:
- The PRE-COMMIT group times out and commits.
- The non-PRE-COMMIT group times out and aborts.
- Inconsistency. The protocol fails its core promise.
In modern distributed systems, network partitions are the expected failure mode (not just node crashes). This makes 3PC unreliable in exactly the environments where distributed transactions matter most.
Additionally:
- Three round-trips instead of two → higher latency.
- More complex recovery logic → more implementation bugs.
- Consensus protocols like Paxos and Raft solve the same problem (non-blocking agreement) more robustly.
When 3PC appears in practice
Almost never as a standalone protocol. Its ideas show up in:
- Database internals (some distributed DBs use 3PC-like phases for DDL changes).
- Academic exam questions (testing understanding of the blocking problem).
- As context for why saga exists — 3PC tried to fix 2PC’s flaw within the atomic-commit paradigm; saga abandons the paradigm entirely.
Saga Pattern
What it is
A saga decomposes a distributed transaction into a sequence of local transactions, each in its own service. Each local transaction has a compensating action that semantically undoes it if a later step fails. There is no global lock, no prepare phase, and no blocking.
Two coordination styles
| Style | How it works | Pros | Cons |
|---|---|---|---|
| Orchestration | A central saga orchestrator tells each service what to do and tracks progress | Explicit state machine; easy to debug and monitor | Single point of coordination; can become a bottleneck |
| Choreography | Each service emits an event on completion; the next service reacts | No central coordinator; loosely coupled | Distributed state is hard to reason about; debugging requires log correlation |
For most interview answers, orchestration is the right default — it’s easier to reason about, easier to explain, and maps directly to the sequence the interviewer expects.
The sequence (orchestrated)
Happy path:
┌─────────────────────────────────────────────────────────────┐
│ Orchestrator Service A Service B Service C │
│ │ │ │ │ │
│ │── Do Step 1 ──►│ │ │ │
│ │◄── Done ───────│ │ │ │
│ │ │ │ │
│ │── Do Step 2 ──────────────────►│ │ │
│ │◄── Done ──────────────────────│ │ │
│ │ │ │
│ │── Do Step 3 ─────────────────────────────────►│ │
│ │◄── Done ──────────────────────────────────────│ │
│ │ │
│ │ [Saga complete] │
└─────────────────────────────────────────────────────────────┘
Compensation path (Step 2 fails):
┌─────────────────────────────────────────────────────────────┐
│ Orchestrator Service A Service B Service C │
│ │ │ │ │ │
│ │── Do Step 1 ──►│ │ │ │
│ │◄── Done ───────│ │ │ │
│ │ │ │ │
│ │── Do Step 2 ──────────────────►│ │ │
│ │◄── FAILED ────────────────────│ │ │
│ │ │
│ │── Compensate Step 1 ──►│ │ │
│ │◄── Compensated ────────│ │ │
│ │ │
│ │ [Saga rolled back] │
└─────────────────────────────────────────────────────────────┘
Step by step (using an order example):
- Reserve inventory → on success, continue. Compensation: release reservation.
- Authorize payment → on success, continue. Compensation: void auth.
- Confirm order → on success, continue. Compensation: mark cancelled.
- Notify shipping → on success, saga complete. Compensation: cancel shipment.
If step 2 fails: orchestrator calls step 1’s compensation (release inventory). No later steps run.
If step 4 fails: orchestrator calls compensations for steps 3, 2, 1 — in reverse order.
Key properties
- No global lock. Each step commits locally and independently. Other transactions can interleave.
- Eventual consistency. Between step 1 completing and step 4 completing, the system is in a partially-committed state. Other observers might see reserved-but-unpaid inventory.
- Compensations are not rollbacks. They’re new forward
operations that semantically reverse the effect. A “void
payment” is a new API call, not an undo. This means:
- Compensations must be idempotent (safe to retry).
- Some effects cannot be compensated (you can’t un-send an email, un-ship a package). Design accordingly.
- Saga state must be durable. The orchestrator’s “I’m at step 3 of 5” state must survive crashes. On restart, it resumes from the last durable step. An in-memory state machine is not sufficient.
The isolation problem
Sagas do not provide isolation (the “I” in ACID). Between steps, intermediate state is visible to other transactions. This creates specific anomalies:
| Anomaly | Example | Mitigation |
|---|---|---|
| Dirty read | Service B reads inventory that step 1 reserved but step 2 hasn’t paid for yet | Semantic locking: mark inventory as “pending” so other readers know it’s not final |
| Lost update | Two sagas both reserve the same last unit of inventory | Claim-based reservation: step 1 atomically decrements; the second saga sees 0 and fails |
| Non-repeatable read | Step 3 reads a price that changed between step 1 and step 3 | Snapshotting: capture price at step 1 and carry it through the saga |
These aren’t bugs in the saga pattern — they’re the inherent cost of giving up global isolation. The fix is always domain-specific (semantic locks, reservations, snapshots), not protocol-level.
When to use saga
- Microservices that own their own databases. Each service has autonomy; 2PC across service boundaries is impractical.
- Long-lived workflows — order processing, booking, onboarding — where holding locks for the full duration would block the system.
- External service calls (payment gateways, shipping APIs) that don’t support prepare/commit.
- When eventual consistency is acceptable and the business can tolerate the “partially committed” intermediate state.
When NOT to use saga
- When you need strong isolation. If two concurrent sagas must not interleave (e.g., financial ledger entries that must balance at every instant), saga’s lack of isolation is a deal-breaker. Use a single database with ACID.
- When compensation is impossible or too expensive. If un-doing step 3 costs more than the entire transaction (e.g., physical goods already shipped), the saga’s compensation model breaks down.
- When the operation is fast and local. If all the data lives
in one database, use a local ACID transaction. Don’t introduce
saga machinery for a problem that
BEGIN; ... COMMIT;solves.
Decision tree: when to use what
Is all the data in one database?
├── YES → Use a local ACID transaction. Done.
└── NO → Are all participants under your control and co-located?
├── YES → Is the operation short-lived (<100ms)?
│ ├── YES → 2PC (XA) is acceptable.
│ └── NO → Use saga (orchestrated).
└── NO → Does any participant not support prepare/commit?
├── YES → Use saga (only option).
└── NO → Is strong isolation required?
├── YES → Redesign to co-locate data, or accept
│ the cost of 2PC blocking.
└── NO → Use saga (orchestrated).
The honest summary: saga is the default for modern distributed systems. 2PC survives inside database clusters and in XA scenarios where all participants are local and fast. 3PC is historical — understand it to explain why saga exists, not to use it.
Comparison: the same operation under each pattern
Scenario: Transfer $100 from Account A (Bank Service) to Account B (Bank Service), with a notification to the user.
Under 2PC
- Coordinator → Bank A: PREPARE (debit $100)
- Coordinator → Bank B: PREPARE (credit $100)
- Both reply YES
- Coordinator → Both: COMMIT
- Both apply; locks release
- Notification sent after commit
Failure: If coordinator crashes between step 3 and 4, both banks hold locks indefinitely until coordinator recovers.
Under Saga (orchestrated)
- Orchestrator → Bank A: Debit $100 (committed locally)
- Orchestrator → Bank B: Credit $100 (committed locally)
- Orchestrator → Notification: Send confirmation
- Saga complete
Failure: If step 2 fails, orchestrator calls Bank A’s compensation: Credit $100 back. The debit was briefly visible (no isolation), but the final state is consistent.
Under 3PC
Same as 2PC but with a pre-commit phase between steps 2 and 4. Under network partition, banks may disagree. Not used for this in practice.
Saga implementation patterns
Orchestrator state machine
The orchestrator is a state machine with transitions:
STARTED → STEP_1_PENDING → STEP_1_DONE
→ STEP_2_PENDING → STEP_2_DONE
→ ...
→ COMPLETED
On failure at any step:
STEP_N_FAILED → COMPENSATING_N-1 → COMPENSATING_N-2
→ ... → COMPENSATED (rolled back)
The state must be persisted (to a database, not in memory) after every transition. On crash recovery, the orchestrator reads its last persisted state and resumes.
Idempotency at every hop
Every service call in a saga must be idempotent — safe to retry without side effects on duplicate. Because:
- The orchestrator may crash after step N succeeds but before it records the success. On restart, it retries step N.
- Network issues may cause the orchestrator to timeout and retry even though the service already processed the request.
Implementation: derive an idempotency key from (saga_id, step_name). Each service stores recently-seen keys and returns
the cached response on duplicate.
Retry before compensate
Not every failure is permanent. A timeout from Bank B might mean “Bank B is slow” not “Bank B rejected.” The orchestrator should:
- Retry transient failures with exponential backoff (3–5 attempts).
- Only trigger compensation after exhausting retries or receiving a permanent failure code.
Compensating prematurely on a transient failure is a bug — it creates unnecessary reversals and confuses users.
Tradeoff summary
| Dimension | 2PC | 3PC | Saga |
|---|---|---|---|
| Consistency | Strong (atomic) | Strong (non-blocking in theory) | Eventual |
| Isolation | Full (locks held) | Full (locks held) | None (intermediate states visible) |
| Availability | Low (blocked on coordinator) | Higher than 2PC | High (no global blocking) |
| Latency | 2 round-trips | 3 round-trips | N sequential steps (parallelizable in some cases) |
| Durability of decision | Coordinator log | Coordinator log | Orchestrator state + service-local commits |
| Failure recovery | Wait for coordinator | Timeout-based | Compensation |
| Works across services you don’t own | No | No | Yes |
| Complexity | Low | Medium | High (compensation logic) |
The interview-ready one-liner for each:
- 2PC: “All-or-nothing, but blocks if the coordinator dies.”
- 3PC: “Tries to fix 2PC’s blocking, but fails under network partitions — replaced by consensus protocols in practice.”
- Saga: “Gives up atomicity and isolation for availability and autonomy. Requires explicit compensation logic for every step.”
Related
- Walkthrough: Designing an Order Processing System — a full walkthrough of orchestrated saga in action, with the compensation path walked through explicitly.
- Walkthrough: Designing a Flash Deal System — the inventory-decrement problem that drives the choice of atomic operations over distributed transactions.
- Reference: SQL vs NoSQL Schema Design — the storage layer these transaction patterns coordinate across.