LearnHLDDesign a global database

Design a globally consistent database

Most distributed databases ask you to give something up. Availability, or transactions, or knowing whether the row you just read is current. The interesting question is whether you can have SQL transactions across continents with a guarantee strong enough that nobody has to think about replicas at all.

Spanner’s answer, and the reason it is worth studying, is that the hard part is not consensus. Consensus is solved. The hard part is time: two machines cannot agree on what “now” means, and every strong ordering guarantee across regions eventually reduces to that.

Your answer

Transaction A commits in Mumbai. Transaction B starts in Frankfurt one millisecond later and reads the same row. Should B see A's write, and how could Frankfurt possibly know?

What you are building

In scope
  • SQL transactions across rows that live in different regions. Serialisable, not eventually consistent.
  • External consistency. If A commits before B starts in real time, every observer sees A before B. This is the strongest ordering guarantee and it is the product.
  • Survive losing a region. Without data loss and without a manual failover decision.
  • Reads that do not block writes. Multi version storage, so a read never takes a lock.
The numbers you commit to
  • Data in at least three regions.
  • Reads served from a nearby replica where possible.
  • Commit latency measured in tens of milliseconds, not hundreds.
  • No lost writes on the failure of any single region.
Cut, and say so out loud
  • Query planning and optimisation, which is a whole discipline and orthogonal to the distribution problem.
  • Schema change coordination, which is genuinely hard and would take the whole session.
  • Analytical queries. This is a transactional store, and mixing the two is a separate design.

The problem is time

What a cross region commit costs
150 ms
7 ms
2
Replicate to a quorumone round trip, half of RTT each way = ~75 ms
Two phase commit across partitions~150 ms
Commit wait2 x uncertainty of 7ms = ~14 ms
Total commit latency~239 ms
~239 ms
to commit, and the clock term is the one you can engineer away

Drag the clock uncertainty slider. At 7ms, which is what you get with GPS receivers and atomic clocks in every datacentre, commit wait costs about 14ms and is a rounding error next to the network. At 100ms, which is what ordinary NTP gives you, commit wait dominates everything and the design becomes unusable. That is why this system exists inside a company that could put atomic clocks in its buildings: tightening the clock bound is what makes the guarantee affordable.

That slider is the whole insight. Every strong consistency guarantee across regions is paid for in either coordination or waiting, and the cost of waiting is proportional to how badly you know the time.

The building blocks

Figure 1. Data splits into partitions. Each partition is its own replicated state machine, and a transaction across partitions coordinates those groups rather than individual machines.

Two layers, and keeping them separate is what makes this tractable. Within a partition, consensus replicates a log across regions and elects a leader, which is Paxos or Raft and is well understood. Across partitions, two phase commit coordinates those groups.

The reason two phase commit is acceptable here, when it was the wrong answer for the payment rail, is that every participant is a fault tolerant consensus group inside one operator’s control. A 2PC participant that can crash and block everyone forever is unacceptable. A participant that survives losing a machine or a region, and is operated by the same team, is a different proposition entirely.

Preset
3
2
2
W + R - N = 1 overlap, so reads see the latest write
Every read set shares at least 1 node with every write set, so a read always touches a replica that has the newest value. Writes survive 1 node failure, reads survive 1.

Each partition replicates across regions and commits when a majority acknowledges. With five replicas a write survives two failures and the commit latency is the third fastest replica rather than the slowest, which is why an odd number and geographic spread both matter.

Deep dive one: why timestamps are the whole problem

Serialisable transactions need an order. The natural way to order them is by commit timestamp. But a timestamp comes from a clock, and two machines never agree.

If Mumbai’s clock is 5ms ahead of Frankfurt’s, a transaction that genuinely commits later in Frankfurt can carry an earlier timestamp. Order the log by timestamp and you have inverted causality: someone reading at that timestamp sees the later write and not the earlier one.

The fix is counterintuitive and elegant. Stop pretending the clock is a point and treat it as an interval. The time service returns not a timestamp but a range, [earliest, latest], and guarantees the true time lies inside it. Then, having chosen a commit timestamp, the transaction waits until that timestamp is definitely in the past everywhere before releasing its locks.

1/6 A is ready to commit and asks for the time. It gets back an interval rather than an instant: true time is somewhere between earliest and latest, and the width of that interval is the uncertainty bound.

That is external consistency, and the trade is stated plainly: you buy a global ordering guarantee by making every commit wait a few milliseconds. The tighter your clocks, the cheaper the guarantee. This is why the same design with ordinary NTP clocks, where uncertainty is hundreds of milliseconds, is not worth building.

What this does not require

No message is exchanged between the two transactions, and no global sequencer exists. People often assume strong global ordering needs a central authority handing out numbers, which would be a bottleneck and a single point of failure. The wait replaces the coordination, and it scales because every node waits independently.

Deep dive two: reads that never block

Every version of every row is kept with the timestamp that wrote it. A read at timestamp T sees exactly the versions committed at or before T, and takes no locks at all, so readers never block writers and writers never block readers.

multi version storagekey and timestamp, ordered
keybytesPKOrdered, so ranges of keys form the partitions.
timestampbigintPKDescending, so the newest version is found first. Assigned at commit, after commit wait.
valuebytesA tombstone marks a delete, since nothing is removed in place.
lockoptionalHeld only by writers, and only between prepare and commit. Readers never take one.
Sample row
user:4471 @ 1756110023107 = {"tier":"pro"}
user:4471 @ 1756109981004 = {"tier":"free"}
Old versions are kept for a garbage collection window, typically an hour or so. That window is what makes consistent snapshot reads across the entire database possible: pick a timestamp slightly in the past and every replica anywhere can serve it locally, with no coordination and no risk of seeing a partial transaction.

This gives you the feature people find most surprising. A read only transaction can pick a timestamp a few seconds old and be served entirely by the nearest replica, in the same region, with no cross region round trip and no leader involvement, while still seeing a globally consistent snapshot. Most read traffic in these systems runs this way, which is what keeps the design usable despite the expensive writes.

Deep dive three: what a cross partition commit actually costs

Transaction shape
One consensus round to a majority, then commit wait. No coordinator, no prepare phase, no other partitions involved. This is the fast path and it is why partitioning by whatever the application transacts over is the single most important schema decision in this system.

Break it

Failure and clock health
healthy
healthyregion B downleader region downclock uncertainty spikesnode evicted
Healthy. Clock uncertainty around 7ms, so commit wait is roughly 14ms and disappears next to the network. Single partition writes commit in tens of milliseconds and stale reads are served locally in single digits.

The API

POST/v1/transactions:commit
{
  "mutations": [...],
  "readSet": [{ "key": "user:4471", "version": 1756109981004 }]
}
returns 200 { commitTimestamp: 1756110023107 }
Why: The commit timestamp comes back to the client. That is what lets a caller do a read at exactly that timestamp later and be certain it sees its own write, without needing sticky routing to a particular replica.
GET/v1/read?key=user:4471&staleness=10s
returns 200 { value, timestamp }
Why: Staleness is a parameter the caller chooses. Ten seconds of staleness turns a cross region quorum read into a local one, and most read paths can accept that. Making it explicit means the caller is choosing latency against freshness rather than the database guessing.
GET/v1/read?key=user:4471&mode=strong
returns 200 { value, timestamp }
Why: The strong option still exists and costs a round trip to the leader. Offering both, with the cost visible in the parameter, is better than one setting that is wrong for half the callers.

Trade-offs

ChoiceWhat you gainWhat you payPick it when
Bounded clock uncertainty plus commit waitExternal consistency with no global sequencer and no messages between unrelated transactions.Every commit waits about twice the uncertainty bound, and you must own the hardware to keep that bound small.You control the datacentres. With ordinary NTP the wait dominates and the design stops being worth it.
Two phase commit over consensus groupsReal cross partition transactions, with participants that survive machine and region failure.Roughly twice the round trips of a single partition commit, with locks held throughout.Participants are fault tolerant and under one operator. Never across organisations.
Multi version storageReaders never block writers, and any replica can serve a consistent snapshot at a past timestamp with no coordination.Storing old versions and garbage collecting them, which bounds how far back you can read.Any system where read traffic dominates and you want it served locally.
Stale reads as a first class optionMost reads become local and single digit milliseconds while remaining globally consistent.The caller has to reason about how much staleness is acceptable for their case.Always offer it. The alternative is every read paying for a guarantee most of them do not need.

Interview replay

Interviewer
Transaction A commits in Mumbai and B starts in Frankfurt a millisecond later. How does B know to order itself after A?
The central question, and most answers reach for a global sequencer.
You
No message passes between them, which is the surprising part. A asks for the time and gets an interval rather than an instant, picks the upper bound as its commit timestamp, and then deliberately waits until that timestamp is definitely past on every clock before releasing its locks and telling its client. So by the time B can possibly start, every clock already reads later than A’s timestamp, and B cannot pick anything earlier. The ordering comes from A being willing to wait rather than from coordination.
The phrase "no message passes between them" is what makes this answer land, because it contradicts the assumption the question invites.
Interviewer
That wait sounds expensive. How expensive?
Checking whether the cost is understood quantitatively.
You
About twice the clock uncertainty bound, so roughly 14ms if you hold uncertainty near 7ms with GPS and atomic clock references in every datacentre. Next to a cross region round trip that is a rounding error. With ordinary NTP, where uncertainty is hundreds of milliseconds, commit wait would dominate everything and the design would not be worth building. Tightening the clock is what makes the guarantee affordable, which is why this comes out of a company that could put atomic clocks in its buildings.
Names the number, compares it to the alternative, and identifies the hardware dependency as the real constraint.
Interviewer
Earlier you said two phase commit was the wrong answer for a payment rail. Why is it acceptable here?
A deliberate consistency check across the whole conversation. Excellent question and easy to fumble.
You
Because the objection to 2PC is blocking: a participant that dies holding locks stalls everyone until it returns. Across two banks that is unacceptable, since neither can force the other to recover. Here every participant is a consensus group that survives losing machines and whole regions, and a coordinator failure is recovered by the same mechanism. The failure mode that made 2PC unusable has been engineered away, so what is left is just the round trip cost.
Identifies the precise property that changed rather than treating the two cases as unrelated. This is the strongest possible answer here.
Interviewer
A datacentre’s time reference fails and clock uncertainty jumps to 300ms. What happens?
Testing whether the failure mode preserves correctness or breaks it.
You
Writes get much slower and stay correct. Commit wait is derived from the uncertainty, so it grows to about 600ms automatically. The system trades latency for safety rather than trading away the guarantee, which is the right direction. A node whose clock drifts beyond what it can justify removes itself from serving, because losing a replica is recoverable and a node issuing timestamps it cannot vouch for would silently break external consistency for everyone.
Degrading latency rather than correctness, plus self eviction, is exactly the behaviour a staff interviewer is probing for.
Interviewer
Most applications cannot afford tens of milliseconds per read. Is this usable?
The practicality question, and the answer is the part people forget.
You
Yes, because most reads should not be strong reads. A read only transaction at a timestamp ten seconds in the past is served entirely by the nearest replica in the same region, with no leader and no cross region traffic, and it still sees a globally consistent snapshot because storage is multi version. That is single digit milliseconds. I would make staleness an explicit parameter so callers choose, and I would expect the large majority of read traffic to take that path.
Turns an apparent weakness into the design’s most useful feature, and ties it back to multi version storage.

Checkpoint

Checkpoint

1. What does commit wait actually achieve?

2. Why is two phase commit reasonable here but not across two banks?

3. Clock uncertainty widens from 7ms to 300ms. What happens to correctness?

Say this in 60 seconds

The hard part is not consensus, it is time. Within a partition, a consensus group replicates across regions and elects a leader, and across partitions two phase commit coordinates those groups, which is acceptable here precisely because every participant is fault tolerant and under one operator, unlike two banks where a blocked participant has no recovery path. Ordering comes from timestamps, and since clocks disagree the time service returns an interval rather than an instant: a transaction picks the upper bound as its commit timestamp and then waits until that timestamp is definitely past everywhere before becoming visible. That gives external consistency with no global sequencer and no messages between unrelated transactions, at a cost of about twice the clock uncertainty per commit, which is why tightening the clock bound with dedicated hardware is what makes the whole thing affordable. Storage is multi version, so readers never take locks and a read at a slightly past timestamp is served by the nearest replica with no cross region traffic while still seeing a consistent global snapshot, which is how most read traffic should actually run. And when clock uncertainty widens, commit wait grows automatically, so the system gets slower rather than wrong.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.