Concept · Databases

Distributed Transactions

01

Why this matters

User clicks "Buy." You must: charge card (payments service) + decrement inventory (warehouse service) + create order (orders DB). All three or none — partial is catastrophic (card charged, no order). In one database, ACID gives this for free. Across services, it doesn't exist.

The sobering fact: true distributed ACID is effectively impossible without paying huge latency and availability costs. Modern systems use three pragmatic alternatives: 2PC (avoid), Saga (use), and the Outbox pattern (the unsung hero).

02

Three approaches

ApproachHowVerdict
2PC (two-phase commit)Coordinator asks all services "can you commit?", then either "commit" or "abort"Blocking protocol. One slow service stalls everything. Coordinator crash = indeterminate state. Avoid.
SagaSequence of local transactions, each with a compensating action if a later step failsEventually consistent. Compensations must be idempotent. Industry standard.
Outbox patternWrite business change + pending event to one local DB atomically; separate process publishes eventGuarantees at-least-once event delivery without 2PC. Pair with sagas. Essential.
03

Saga — the checkout example

Model the checkout as a sequence. Each step can fail; each step has a compensation that undoes it.

2PC coordinator pseudo-code
# 2PC (expensive, blocks on coordinator failure — prefer sagas where possible)

class Coordinator:
    def commit(self, txn_id, participants):
        # Phase 1: PREPARE
        votes = []
        for p in participants:
            try:
                votes.append(p.prepare(txn_id))  # writes UNDO + REDO log
            except Exception:
                votes.append(False)
        if not all(votes):
            for p in participants: p.rollback(txn_id)
            return "aborted"

        # Phase 2: COMMIT
        self.log.write(f"COMMIT {txn_id}")  # durable decision point
        for p in participants:
            while True:
                try: p.commit(txn_id); break
                except Exception: continue   # infinitely retry
        return "committed"

# Problem: coordinator crash between phase 1 and phase 2 blocks every
# participant holding prepared locks. This is why sagas + idempotency
# wins for most distributed-transaction use cases.
Saga FlowMermaid
sequenceDiagram participant O as Orders participant P as Payments participant I as Inventory participant S as Shipping O->>P: charge($50) P-->>O: charge-id=c42 O->>I: reserve(item-7) I-->>O: reserved O->>S: ship(order-99) S-->>O: shipping failed! Note over O: Compensating chain O->>I: unreserve(item-7) I-->>O: ok O->>P: refund(c42) P-->>O: refunded Note over O: Order rolled back, eventually consistent

Two orchestration styles:

  • Orchestrated saga — central orchestrator (e.g., state machine) tells each service what to do. Clear, debuggable, introduces a new service to own.
  • Choreographed saga — each service listens to events and decides what to do. No central orchestrator. Simpler infra, harder to reason about.

Orchestrated wins for complex workflows (5+ steps). Choreographed wins for simple 2–3 step flows.

04

Why 2PC is bad, in detail

  1. Blocking. All participants lock their resources until the coordinator says go/abort. One slow participant stalls the entire transaction. Under load, lock queues explode.
  2. Coordinator SPOF. If the coordinator crashes after "commit" is sent to one participant but not others, the remaining participants don't know the outcome. They sit holding locks — indefinitely.
  3. Unavailable during partitions. Network partition → coordinator or participant unreachable → entire transaction blocked. CAP CP behavior, but applied to every transaction across every service.

Message brokers (Kafka) and some DBs (Spanner via Paxos) offer transactional guarantees that feel like 2PC but with much better availability via consensus. Outside those narrow cases, don't roll your own 2PC.

05

Deep dive — the Outbox pattern

The core problem with saga: when service A commits its local DB and needs to notify service B, if the notification fails, saga breaks. You cannot atomically "commit DB + send message" across systems (that's 2PC, which we're avoiding).

Outbox pattern: service A writes its DB change AND an "outbox" row in the same local transaction. A separate process polls outbox rows and publishes them to Kafka / a message bus. After successful publish, outbox row is marked sent (or deleted).

  1. DB commit happens: both business data + outbox row are written atomically (one-DB transaction — free).
  2. Poller sees new outbox rows, publishes to Kafka.
  3. If poller crashes after publishing but before marking sent → republishes on restart → at-least-once delivery. Consumers must be idempotent.
  4. If poller crashes before publishing → retries on restart → still at-least-once.

Net effect: atomicity between "change the DB" and "emit the event," without a distributed transaction. Every serious microservices system uses this (Debezium makes it turnkey via DB change-data-capture). Pair with idempotent consumers and sagas — this is the modern distributed transaction stack.

06

Real-world

Stripe

Sagas + idempotency keys

Every payment flow is a saga with compensating refunds. Client sends Idempotency-Key header on every request — retries don't double-charge.

Uber Cadence / Temporal

Workflow-as-code

Orchestrated sagas written as normal code with durable state. Retry, compensate, timeout handled by the platform. What you want when sagas get complex.

Debezium

CDC-based outbox

Streams DB changes (Postgres WAL, MySQL binlog) to Kafka. Turns any DB write into an event automatically.

AWS Step Functions

Managed orchestration

State-machine definition; each step invokes a Lambda or service. Retries, timeouts, compensations all declarative.

07

Used in problems

E-commerce checkout is a saga (payment + inventory + order + notification). Payment gateway uses outbox for webhook delivery. Ticketmaster uses saga + distributed locks to prevent double-booking. Bidding platform uses orchestrated sagas for auction settlement.

Next up