Filter concepts by levelShowing all levels.

System Design · Distributed Transactions

Distributed Transactions

Why cross-service atomicity is structurally hard, what 2PC actually costs, and why sagas are usually preferred

  • No shared boundaryeach database is only ACID with itself
  • Two-phase commitprepare/vote, then commit or abort — with real cost
  • Blocking riska coordinator crash can strand every participant
  • Sagas preferredlocal transactions + compensating actions

1.Why cross-service ACID transactions are difficult

A single database can give ACID guarantees (atomicity, consistency, isolation, durability) across multiple writes within that one database, because it controls the locking, logging and commit process for everything it manages. A workflow spanning multiple independent services, each with its own database, has no single component in that position — no one process can atomically lock rows across two different databases, no one process can guarantee both writes commit or both roll back together, because each database only knows how to be ACID with itself, not in coordination with another database it has never heard of.

Key features

  • A single database's ACID guarantee only covers operations it directly manages — it has no mechanism to make a write in a completely different database part of the same atomic unit
  • Atomicity across services would require every participating service to agree, at the same instant, to either all commit or all roll back — but each service can independently succeed, fail, or become unreachable at any point, with no shared clock or coordinator by default
  • Isolation across services is even harder: a single database can lock a row to hide an in-progress change from other transactions; there is no equivalent cross-service lock that prevents another service from reading a "half-done" cross-service operation partway through
  • Network partitions make the problem worse than a single-machine failure — a service can be unreachable rather than simply down, so a coordinator cannot even reliably tell "this service failed" apart from "this service succeeded but its confirmation was lost in transit"
  • This is not a solvable engineering gap waiting for a better tool — it is a fundamental consequence of each service owning its own independent commit boundary, which is exactly why distributed transaction patterns (two-phase commit, sagas) exist as different trade-offs rather than a single "just make it ACID" fix

What a single database's ACID guarantee covers vs a cross-service operation

What a single database's ACID guarantee covers vs a cross-service operation
PropertySingle databaseCross-service operation
AtomicityGuaranteed — one commit log, one decisionNo shared commit boundary across services
IsolationRow/table locks hide in-progress workNo cross-service lock mechanism exists by default
Failure visibilityA crash is locally detectableA service can be unreachable, not just down — ambiguous from the outside
Coordination costBuilt into the database engineMust be explicitly built (2PC, saga, or accepted as a risk)
No shared commit boundary between two databases
succeedsthen

INSERT order

Orders service, own DB

Order committed

fully consistent, alone

UPDATE inventory

briefly unreachable

  • INSERT order — Orders service, own DB
    • leads to Order committed (succeeds)
  • Order committed — fully consistent, alone
    • on error, leads to UPDATE inventory (then)
  • UPDATE inventory — briefly unreachable

Signature

each database is only ACID with itself — no shared commit boundary exists across services by default

A single database's ACID guarantee only covers what it directly manages — there is no built-in mechanism for making a write in one database part of the same atomic, isolated unit as a write in a completely separate database. The inconsistency risk lives in the space between services, which no single database's own transaction boundary was ever designed to cover.

2.Two-phase commit, and why it is expensive and fragile

Two-phase commit (2PC) is a protocol that lets a coordinator get all participating services to agree on committing or aborting a transaction together. In the first phase (prepare/vote), the coordinator asks every participant to prepare the transaction and vote yes or no; each participant locks its resources and votes. In the second phase (commit), if every participant voted yes, the coordinator tells everyone to commit; if any participant voted no, the coordinator tells everyone to roll back. It genuinely does provide atomicity across services — the cost is that every participant has to hold its locks and stay blocked between the two phases, and if the coordinator itself fails after collecting votes but before sending the final decision, participants can be left blocked indefinitely, unable to release their locked resources.

Key features

  • Phase 1 (prepare/vote): the coordinator asks every participant to prepare the transaction; each participant does the work, writes it to a durable log, locks the relevant resources, and votes yes (ready to commit) or no (cannot commit)
  • Phase 2 (commit/abort): if every participant voted yes, the coordinator tells all participants to commit; if any participant voted no, the coordinator tells all participants to roll back using their logs
  • The blocking problem: once a participant votes yes in phase 1, it must hold its locks and wait for the coordinator's phase 2 decision — it cannot unilaterally decide to commit or abort on its own, even if it never hears back
  • Coordinator failure is the critical fragility: if the coordinator collects all yes votes but crashes before sending the phase 2 decision, every participant is left blocked indefinitely, holding locks it cannot safely release, because none of them can be certain what the other participants decided
  • The cost is proportional to how long resources stay locked across the two phases and how many participants are involved — this is why 2PC is generally avoided for long-running or high-latency operations, and largely avoided across service boundaries in modern distributed systems in favor of sagas (the next concept in this section)

The two phases of 2PC

The two phases of 2PC
PhaseCoordinator actionParticipant action
1: Prepare/voteAsks every participant to prepare and voteLocks resources, logs the pending transaction, votes yes/no
2: Commit/abortCollects votes; tells all to commit (all-yes) or abort (any-no)Commits and releases locks, or rolls back using its log
Two-phase commit: prepare, then a coordinator crash
Coordinator
P1
P2
P3
  1. 1PREPARE
  2. 2PREPARE
  3. 3PREPARE
  4. 4vote yes
  5. 5vote yes
  6. 6vote yes
  7. 7crashes3/3 yes collected, decision never sent — all locked, blocked indefinitely
  1. Coordinator → P1: PREPARE
  2. Coordinator → P2: PREPARE
  3. Coordinator → P3: PREPARE
  4. P1 → Coordinator: vote yes
  5. P2 → Coordinator: vote yes
  6. P3 → Coordinator: vote yes
  7. Coordinator → Coordinator: crashes (3/3 yes collected, decision never sent — all locked, blocked indefinitely)

Command

text
Coordinator -> P1, P2, P3: PREPARE
P1, P2, P3  -> Coordinator: VOTE (yes/no)

if all votes == yes:
    Coordinator -> P1, P2, P3: COMMIT
else:
    Coordinator -> P1, P2, P3: ABORT

Two-phase commit genuinely achieves cross-service atomicity: prepare/vote, then commit-or-abort based on unanimous agreement. The cost is real — every participant holds its locks for the entire window between the two phases, and a coordinator crash after collecting all yes votes but before sending the final decision leaves every participant blocked indefinitely, unable to safely decide on its own.

3.Preferring sagas, local transactions and compensating actions over 2PC

Rather than trying to make a cross-service operation atomic upfront the way two-phase commit does, a saga breaks it into a sequence of local transactions — each one fully committed within its own service, with no cross-service locks held at any point. If a later step fails, the saga does not roll back a single distributed transaction; instead, it runs compensating actions that undo the effect of the earlier steps that already committed. This trades 2PC's strong, immediate cross-service atomicity for a design where each individual step is fast and non-blocking, at the cost of the system passing through real intermediate states that a saga's compensation logic has to be built to handle correctly.

Key features

  • A saga is a sequence of local transactions, each committed independently within its own service — no locks are held across services at any point, unlike 2PC
  • A compensating action is a separate operation that semantically undoes an already-committed step (e.g. "refund the charge" undoes "charge the card") — it is not a database rollback, because the original step already committed and other transactions may have already observed its effect
  • Because each step commits independently, the overall system passes through real, observable intermediate states while a saga is in progress (e.g. an order that exists but has not yet been paid for) — this is the direct trade-off for avoiding 2PC's blocking locks
  • Compensating actions are not always perfect inverses — a cancellation fee, a partially-shipped item, or an already-sent notification email cannot always be undone as cleanly as the original action was performed, which has to be an accepted, designed-for cost, not an afterthought
  • This concept introduces the general reasoning for preferring sagas over 2PC; the saga pattern's own mechanics — choreography vs orchestration, and designing compensating actions systematically — get their own full treatment in the next section

2PC vs saga, the same underlying trade-off

2PC vs saga, the same underlying trade-off
PropertyTwo-phase commitSaga
Resources locked across services?Yes, for the whole protocol durationNo — each local transaction commits independently
Intermediate states visible?No — atomic from the outsideYes — real intermediate states exist while in progress
Recovery from a late failureCoordinated rollback (or blocking if the coordinator fails)Explicit compensating actions, run per completed step
Failure mode under coordinator/participant crashCan block indefinitelyDegrades to "needs the remaining compensations to run" — recoverable without blocking
Two-phase commit vs. saga

Two-phase commit

  • Locks held for the whole protocol duration
  • Atomic from the outside, no visible intermediate states
  • Coordinator crash can block every participant indefinitely

Saga

  • Each local transaction commits independently
  • Real intermediate states are visible while in progress
  • A late failure runs explicit compensating actions
  • Two-phase commit
    • Locks held for the whole protocol duration
    • Atomic from the outside, no visible intermediate states
    • Coordinator crash can block every participant indefinitely
  • Saga
    • Each local transaction commits independently
    • Real intermediate states are visible while in progress
    • A late failure runs explicit compensating actions

Command

text
# saga: sequence of local transactions + compensations
step 1: charge_card()         compensate: refund_card()
step 2: reserve_inventory()   compensate: release_inventory()
step 3: schedule_shipping()   compensate: cancel_shipping()

# if step 3 fails after 1 and 2 already committed:
run compensate for step 2, then compensate for step 1
(in reverse order of the original steps)

A saga trades 2PC's cross-service locking for a sequence of independently-committing local transactions plus compensating actions to unwind a partial failure. Each step is genuinely final the moment it commits, real intermediate states are visible while the saga is in progress, and compensating actions are new operations designed per step, not automatic symmetric undos.

Remember

  • Each database's ACID guarantee only covers itself — there is no built-in mechanism spanning two independent databases.
  • 2PC genuinely achieves cross-service atomicity, at the cost of every participant holding locks for the entire prepare-to-decision window.
  • A coordinator crash after collecting all yes votes but before the final decision leaves every participant blocked indefinitely — no safe unilateral guess exists.
  • A saga trades locking for visible intermediate states and explicit, per-step compensating actions that are not automatic symmetric undos.

Pro tip

  • Confirm an operation genuinely needs cross-service atomicity before reaching for 2PC or a saga — a short, tolerable inconsistency window is often acceptable.
  • Design each compensating action explicitly per step — do not assume one exists automatically just because the forward action does.

Next up

  • Saga pattern
  • Transactional outbox and inbox
  • Distributed locks

Sources

  • Two-phase commit protocol — en.wikipedia.org, fetched 2026-08-22
  • Saga pattern — microservices.io/patterns/data/saga.html
  • Checked 2026-08-22

Spotted a mistake on this page? Tell us.