Skip to main content

Cross-Shard Operations & Distributed Transactions

Level 3: Level 3: Scaling the Data Tierhard35 mincross-shardsagatransactions

Avoid 2PC on the hot path (locks, blocks on coordinator failure); use sagas of local transactions with compensations, idempotency keys, and the outbox pattern.

Sharding breaks joins and atomic multi-key writes

Sharding buys scale by breaking two things you took for granted on a single database: joins and atomic multi-key writes. Once related rows can live on different nodes, a query that spans them is a distributed operation, and a write that must change both is a distributed transaction.

Check yourself
Each of your 50 shards answers reads with a 'p99' of 10ms. A query fans out to all 50 shards and must wait for every one of them. About what fraction of these queries will see at least one shard running slower than its 'p99'?

Cross-shard reads (scatter-gather)

A query that is not scoped to one shard key must fan out to every partition, and it is bounded by the slowest shard, not the average. This is tail latency amplification: if each shard's p99 is 10ms and you hit 50 shards, the chance that at least one is slow is 1 minus 0.99^50, roughly 40%, so the overall p99 is far worse than 10ms. Mitigations: avoid the fan-out by choosing the shard key to match the query, denormalize so the data you need is co-located, cap the fan-out width, and use hedged/speculative requests to blunt single-shard tail latency. The senior instinct is to design most reads to touch one shard and treat scatter-gather as the rare, budgeted case.

Check yourself
You must atomically debit an account on shard A and credit an account on shard B. Two-phase commit gives exactly the atomicity you need. Should it be your default for this hot path?

Cross-shard writes: avoid 2PC on the hot path

The textbook answer is two-phase commit (2PC): a coordinator asks all participants to prepare (lock and promise), and if all vote yes, tells them to commit. It gives atomicity, and you should know it, but avoid it on the hot path. 2PC holds locks across a network round trip, so it kills throughput, and it is blocking: if the coordinator dies after prepare, participants sit holding locks indefinitely, unsure whether to commit or abort. It is defensible only for low-frequency, must-be-atomic operations, and modern systems (Spanner, CockroachDB) make it viable only by pairing it with tight clock/consensus machinery you do not want to hand-roll.

Sagas: the standard replacement

A saga breaks the operation into a sequence of local transactions, each on one shard, and for every step defines a compensating action that semantically undoes it. If a later step fails, you run the compensations for the completed steps in reverse. A money transfer becomes: debit A (local), credit B (local); if credit fails, compensate by re-crediting A. You give up isolation (intermediate states are visible, so you design for them, e.g. a "pending" balance) in exchange for no distributed locks and independent, available shards. Sagas are orchestrated (a central coordinator drives steps, easier to reason about and monitor) or choreographed (each step emits an event the next reacts to, more decoupled but harder to trace).

Two patterns make sagas safe under real-world at-least-once delivery:

  • Idempotency keys. Every step carries a unique key; the receiving shard records processed keys and dedups retries, so replaying "credit B" after a timeout does not double-credit.
  • The outbox pattern. The problem: you must both write the DB row and publish an event, but they are separate systems, so a crash between them loses one. The fix: in the same local transaction, write the business row and an "outbox" row; a separate relay reads the outbox and publishes to Kafka, marking rows sent. Now the DB write and the intent-to-publish are atomic, and the relay retries publishing idempotently. This is how you drive a saga's next step reliably.

Interview nuance: the failure mode interviewers hunt for is hand-waving cross-shard joins and multi-key writes as if they were free. When the design crosses shards, say it: "this is now a distributed transaction; I will use a saga with compensations and idempotency keys, not 2PC on the hot path, and I will denormalize to keep the frequent reads single-shard."

Recap: sharding breaks joins (scatter-gather, bounded by the slowest shard) and atomic multi-key writes; avoid 2PC on the hot path because it locks and blocks on coordinator failure; use a saga of local transactions with compensating actions, make retries safe with idempotency keys, publish events atomically with the outbox pattern, and denormalize to avoid cross-shard joins.

Check yourself

Before you design the cross-shard money transfer, match each mechanism to the failure it exists to prevent.

Idempotency key recorded in a per-shard dedup table
Outbox row committed in the same local transaction as the business write
Relay that re-reads the outbox and republishes until acknowledged
Saga of local transactions with compensating actions

Apply

Your turn

The task this lesson builds to.

Design a money-transfer or order-checkout flow that must update two records living on different shards without losing consistency.

Think about

  1. Why avoid 2PC on the hot path, and what does it cost?
  2. How does a saga with compensations replace a cross-shard transaction?
  3. How do the outbox pattern and idempotency keys make it safe?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the order-placement flow for an Amazon-scale checkout that must, as one logical operation, reserve inventory (inventory service/shard), charge payment (payment service/shard), and create the order (order service/shard), each on a different data store, at tens of thousands of orders/sec. Guarantee no oversell and no double-charge, and keep it available if any one service is briefly down.

Think about

  1. Which local mechanism guarantees no oversell without any cross-service lock?
  2. What happens to the saga when the payment service is down for two minutes?
  3. Why must inventory holds eventually expire?