Skip to lesson
‹

Courses / System Design Interview / Chapter 7

Module 2 · Building Blocks · Chapter 7 of 39

Databases: Replication, Partitioning and Choosing a Store

19 sections · ~33 min✓ Fact-checked

Builds on Back-of-the-Envelope Estimates, Abstractions That Hide the Network and Availability, Reliability, Scalability and Latency. Next up: Consensus, Leader Election and Coordination.

Part 1 · Choosing and Modelling the Data

Why the database is the hard part

It is 8:45 pm on the night of a cricket World Cup final, and the innings break has just begun. Across India, people open the same food delivery app at once: 1,000 orders a second, about eight times the daily average, and twenty menu and tracking reads for every order. The fifty stateless app servers cope. The one database behind them does not. Its CPU sits at 100%, orders take eight seconds to confirm, and at 8:52 it crashes. For 40 seconds, nobody can order anything.

Stateless servers, from the availability chapter, work only because the state lives in the database, and you cannot run ten copies of it and hope they agree. A growing database hits three walls, each with its own tool.

WallOn final nightMain tool
Too many reads20 reads for every orderReplication: copies that serve reads
One machine failsThe 8:52 crashReplication plus failover to a copy
Too many writes or too much data10 million orders a day at 2 KB: about 7 TB a yearPartitioning (sharding): split the data across machines

Large systems use both tools, partitioning the data and replicating each partition. First, though, the choice of database and layout decides how far one machine can go.

Do not shard on day one

One relational primary often takes thousands to tens of thousands of simple writes a second. The app's peak of about 5,000, roughly five per order, fits; the crash came from reads and from having no standby.

Families of databases

The first decision is which kind of database to use. Families differ in the shape of the data they store and the questions they answer fast, so choose by the queries your product runs most, not by fashion.

FamilyData looks likeFast atWeak atGood for
Relational (SQL)Rows in tables with a fixed schema (declared columns), linked by keysJoins, transactions, ad hoc queriesWrites beyond one primaryOrders, payments; most apps at first
Distributed SQLTables split into ranges, each on 3 machines that commit by majority voteSQL and transactions, with writes spread over nodesEvery commit waits for a majorityRelational data past one primary
Key-valueA key and an opaque valueGet and put by key, often under 1 msAny query not by keyCarts, sessions, rider locations
DocumentJSON-like documents whose fields can varyReading a whole recordJoins and many-to-many linksMenus, profiles
Wide-columnRows grouped by a partition key, sorted by a second keyHeavy writes; one key's rows in orderQueries outside the key designOrder events, chat history
GraphNodes and the edges between themFollowing links many hops deepBulk scans; splitting across machinesFraud rings, friend suggestions
Relational (SQL)

Data looks likeRows in tables with a fixed schema (declared columns), linked by keys

Fast atJoins, transactions, ad hoc queries

Weak atWrites beyond one primary

Good forOrders, payments; most apps at first

Distributed SQL

Data looks likeTables split into ranges, each on 3 machines that commit by majority vote

Fast atSQL and transactions, with writes spread over nodes

Weak atEvery commit waits for a majority

Good forRelational data past one primary

Key-value

Data looks likeA key and an opaque value

Fast atGet and put by key, often under 1 ms

Weak atAny query not by key

Good forCarts, sessions, rider locations

Document

Data looks likeJSON-like documents whose fields can vary

Fast atReading a whole record

Weak atJoins and many-to-many links

Good forMenus, profiles

Wide-column

Data looks likeRows grouped by a partition key, sorted by a second key

Fast atHeavy writes; one key's rows in order

Weak atQueries outside the key design

Good forOrder events, chat history

Graph

Data looks likeNodes and the edges between them

Fast atFollowing links many hops deep

Weak atBulk scans; splitting across machines

Good forFraud rings, friend suggestions

Relational is the usual start; every other family gives up general queries to serve one access pattern very well. Time-series stores and search indexes, met later, usually sit beside a main database.

Names you will hear

PostgreSQL and MySQL: relational, the usual default. CockroachDB, YugabyteDB and Spanner: distributed SQL. Redis: in-memory key-value. DynamoDB: managed key-value at very large scale. MongoDB: documents. Cassandra: wide-column. Neo4j: graphs.

Modelling the data

Next, lay out the data. The food app has restaurants, dishes and orders, and two broad choices.

Normalising stores each fact once, like a contacts list that keeps each number in one place. An order points to its restaurant by restaurant_id, so a rename changes one row, and reads join the tables (combine matching rows) to show the name. Denormalising copies data to where it is read: each order also stores the restaurant's name, so "my orders" needs no join, but a rename must update every copy.

NormalisedDenormalised
Each fact is storedOnceIn several places
ReadsJoin a few tablesOne row or document
WritesChange one placeChange every copy, or accept stale ones
Start here whenData changes often and must stay correctOne hot read path is measured as too slow

Normalise first, then denormalise one measured hot path at a time. Some apparent copies are separate facts: an order's price is what was paid on 3 March, and must not change with the menu.

Key-value and wide-column stores have no joins, so you model by query: list the queries first and give each its own table. "A customer's orders, newest first" is keyed by customer_id; "a restaurant's orders today" needs a second table keyed by restaurant_id, written on every order. In a document store, embed what is read together, such as an order's items, and reference what grows without limit, such as reviews.

The catch with query-first models

A query nobody planned for may need a new table and a backfill of every old row. In a relational model it is often one new index, the subject of the next page.

Part 2 · Inside One Database

Indexes: finding rows without reading them all

The restaurant dashboard asks for restaurant 77's orders today. Without help, the database reads every row, a full scan: many seconds across 50 million orders. An index works like the index at the back of a textbook: a separate sorted structure that maps a column's values to their rows, so the lookup takes about a millisecond.

Most relational databases store tables and indexes as B-trees: shallow trees of fixed-size pages sorted by key, each pointing to hundreds of children, so a billion rows sit 3 or 4 levels deep. Write-heavy stores often use LSM trees (log-structured merge trees): writes go to a log and to memory, then out to sorted files that merge in the background. These two storage engines shift work between writes and reads.

B-treeLSM tree
A writeFinds its page and changes it in place: a random writeAppends to a log and to memory: sequential writes
A readOne walk from the root to a leafMemory, then several files; filters skip most
SuitsRead-heavy and mixed workWrite-heavy work: events, logs, metrics
Found inPostgreSQL, MySQLCassandra, RocksDB

LSM trees make writes cheap and pay later, in background merging that uses disk bandwidth; for mixed work a B-tree is the safer default.

An index on (restaurant_id, created_at) is sorted by restaurant, then by time, like a phone book sorted by surname, then first name. It answers the dashboard with one short range read, but not a query on created_at alone: put equality columns first, the range column last. An index that also holds every column the query needs is a covering index: the table is never touched.

Every index costs writes

Each insert must update every index too, so a table with 5 indexes does about 6 writes per new order. Index the queries you run; drop unused indexes.

ACID, briefly

Fast reads are half the job; the other half is staying correct while many orders happen at once. A transaction groups several reads and writes into one unit, and relational databases give it four promises known as ACID. Take Asha paying ₹450 for biryani from her app wallet: the wallet is debited and an order row is created.

LetterPromiseIn Asha's order
AtomicityAll of it happens, or none of itNever a debit without the order, even if the server crashes halfway
ConsistencyRules you declared still holdThe wallet never goes below zero, if a rule says so
IsolationTransactions running at once do not interfereAt the strictest level, two orders at once cannot both spend the same ₹450
DurabilityOnce confirmed, the change survives a crashAfter "order placed", a power cut does not undo it

Atomicity and durability come from the write-ahead log: the database appends every change to a log on disk before applying it, so after a crash it redoes committed work and discards the rest. Isolation is the subtle letter, and the next page shows where it leaks.

Two different Cs

The C in ACID means your own rules hold. It is not consistency between replicas, from the abstractions chapter.

Many key-value and document stores drop or limit multi-row transactions to scale more easily, keeping only single-item writes atomic. If a core flow must change several rows together, like Asha's debit and order, start relational.

Isolation levels and what slips through

Isolation comes in levels, like the gap between one cashier serving a queue and two cashiers sharing one cash box. Serializable behaves as if transactions ran one at a time. Most databases default to something weaker that blocks less: read committed, where you never see another transaction's uncommitted changes, or snapshot isolation, where each transaction reads one frozen, consistent view. Each weaker level lets a known anomaly through.

AnomalySlips through underExampleFixes
Lost updateRead committedAsha's wallet holds ₹1,000. Orders of ₹300 and ₹200 both read 1,000, then write 700 and 800; one payment vanishesAn atomic update (SET balance = balance - 300), SELECT … FOR UPDATE, or snapshot isolation, which aborts one of the two writers
Write skewSnapshot isolationRule: one active order per rider. Two dispatchers both see rider 7 free and each insert a different assignment row; no row is written twice, so both commitLock the rider row and set its order there, a conditional update, a unique constraint, or serializable
Lost update

Slips through underRead committed

ExampleAsha's wallet holds ₹1,000. Orders of ₹300 and ₹200 both read 1,000, then write 700 and 800; one payment vanishes

FixesAn atomic update (SET balance = balance - 300), SELECT … FOR UPDATE, or snapshot isolation, which aborts one of the two writers

Write skew

Slips through underSnapshot isolation

ExampleRule: one active order per rider. Two dispatchers both see rider 7 free and each insert a different assignment row; no row is written twice, so both commit

FixesLock the rider row and set its order there, a conditional update, a unique constraint, or serializable

Snapshot isolation catches two writers on the same row, but not two writers on different rows that together break a rule.

Every fix makes the check and the write happen as one step. A conditional update does both in one statement: UPDATE riders SET order_id = 42 WHERE id = 7 AND order_id IS NULL changes the row for the first dispatcher and nothing for the second. SELECT … FOR UPDATE locks the rows you read until you commit. A unique constraint refuses the second row. Serializable aborts one transaction, so your code must retry.

Names differ

One database's "repeatable read" stops lost updates; another's does not. Check what yours promises. The ticket booking chapter applies these fixes to seats.

Part 3 · Replication: Keeping Copies

One leader, many followers

Indexes and isolation make one machine fast and correct, but it can still crash at 8:52. Replication keeps copies on other machines. Picture a head cashier who alone writes in the ledger, while assistants copy each entry into their own books and answer questions from them.

In single-leader replication, one node, the leader (or primary), accepts every write. It records each change in a replication log and streams it to the other nodes, the followers (or replicas), which apply the changes in the same order. A follower can answer reads, and it is a warm standby if the leader dies.

On final night the app reads 20 times more than it writes. Send menu and tracking reads to followers, and each one adds read capacity: if a machine serves 20,000 simple reads a second, five followers serve about 100,000. But every write still goes through one leader, so followers never add write capacity.

Each follower also stores a full copy and replays every write. Two questions follow: does the leader wait for followers before confirming, and what does a reader see on a follower that is behind?

Synchronous or asynchronous

When a write reaches the leader, the leader can wait for a follower before it says "done", or reply at once and copy in the background, like a shop that updates the second ledger before handing over the receipt, or after.

SynchronousAsynchronous
Leader replies afterA follower confirms it has the changeWriting the change locally
Write latencyPlus a round trip to that follower: about 1 ms across zonesLowest
That follower is downWrites stall until it returns or is droppedWrites carry on
The leader diesNo confirmed write is lostThe last few confirmed writes can be lost

Synchronous copying buys zero loss with latency and a risk of stalls; asynchronous copying buys speed with a small loss window.

Most systems take a middle path, semi-synchronous replication: the leader waits for any one follower, so every confirmed write exists on two machines, and the rest copy in the background. One slow follower then stalls nothing, because another can answer. For orders and the wallet, that extra millisecond is cheap insurance; rider locations, replaced every few seconds, can stay asynchronous.

With asynchronous followers, the gap behind the leader is the replication lag: usually well under a second, but seconds or minutes under heavy load. Lag loses nothing, yet it can show a user an old version of their own data.

Reading your own writes

Lag stays invisible until someone reads their own change. Kiran is about to order dinner and fixes the flat number in the delivery address.

Animated sceneA stale read right after a write0:21

Watch for which copy answers the next read. Kiran saves a new delivery address

Read it as steps

Kiran fixes the delivery address, then reads it back from a follower that is still behind.

Nothing was lost, yet the save looked broken, and a rider might have gone to the old flat. Kiran needs read-your-writes, from the abstractions chapter, and routing provides it, not a faster database. Four routing rules give users the guarantees they expect:

  • Read your own data from the leader: a customer's own address and orders from the leader; menus from followers.
  • Pin to the leader briefly: for a minute after a write, that user's reads go to the leader.
  • Wait for the follower: read only from a follower that has applied the user's last write position.
  • Stick each user to one follower, so a refresh never shows older data than the last: monotonic reads.

Each rule moves some reads back to the leader, so keep them narrow: route a user's own data, not every page.

Failover: a majority vote and fencing

Back to 8:52: the leader crashes, and a follower must take over, which is called failover. The hard part is deciding safely that the leader is dead, and who replaces it. A small consensus group of three nodes decides by majority vote, and each new leader gets a higher term, a number that goes up with every election.

Predict before you watch: the leader crashes and returns 30 seconds later, still believing it leads. What happens to writes meanwhile, and what stops two leaders?
  • Writes fail until the group's timeout expires; then 2 of 3 votes promote a follower in term 2.
  • The old leader returns with the older term 1, so it is refused and rejoins as a follower.
Animated sceneReads spread out, then the leader fails0:26

Watch for the old leader when it comes back. First, writes go to the leader

Read it as steps

A 3-node group elects a new leader, then fences the old one when it returns.

The vote makes failover safe: a watcher on one machine would be a single point of failure, and in a network split only the side with a majority can elect a leader. Fencing refuses anything carrying an old term. Failover is not instant, though: writes fail until the timeout fires, often several seconds to half a minute.

What failover can break, and backups

Failover sounds simple: notice the dead leader, promote a follower, send writes to it. Each step hides a trap, and the defences are where most of the cost goes.

Failover pitfallWhat happensUsual defence
Lost writesThe old leader confirmed writes it had not yet shipped to an asynchronous follower; the new leader never had themSemi-synchronous copying, and promote the follower that confirmed
Split brainThe old leader was cut off, not dead, and keeps accepting writes from clients that can still reach itMajority vote, fencing by term, and a leader that steps down when it cannot reach a majority
Wrong timeoutToo short: a leader slowed by a spike is replaced, and the failover adds load. Too long: minutes of failed writesTune it to measured pauses; some teams have a person confirm failover

Each defence costs something: latency, extra machines or slower recovery. Voting groups run on 3 or 5 nodes across zones, surviving 1 or 2 failures; the next chapter shows how they vote.

Replication is not a backup

A follower copies every change within milliseconds, including a DELETE with a missing WHERE. Keep real backups: a daily snapshot plus the archived write-ahead log, which together restore the database to any second before the mistake. That is point-in-time recovery. Replaying 20 hours of log can itself take hours, so measure restore time, keep backups in another account or region, and test them; the outages chapter shows why.

Multi-leader and leaderless

Single-leader replication sends every write to one place. Two other designs remove that limit, and both pay with write conflicts: two copies accepting different values for the same row.

Multi-leader replication runs one leader per region. If the food app expands to the Gulf, a manager in Mumbai writes to the Indian leader, one in Dubai writes to the Gulf leader, and the leaders exchange changes in the background. Writes stay fast, and one region can fail alone. But if both managers change the same dish's price at once, both edits succeed locally, then collide. Three strategies handle that.

Conflict strategyHow it worksRisk
Last write winsKeep the version with the latest timestampSilently drops the other write; clock skew can pick the wrong one
MergeKeep both, then combine them or ask a personNeeds merge logic for each kind of data
AvoidGive each record a home region that takes all its writesWrites from far away are slow again

Choose multi-leader only when several regions must write fast or keep writing through a regional outage; offline-first apps, which sync each phone's edits later, are multi-leader too.

Leaderless replication drops the leader. The client sends each write to all N replicas and counts it successful once W confirm; a read asks R replicas and keeps the newest version. If R + W > N, the two groups share a replica, so the read sees the latest successful write. N = 3, W = 2, R = 2 overlaps and survives one replica down; W = 1, R = 1 is fastest but can miss the last write. The key-value store chapter builds this design.

Part 4 · Partitioning: Splitting the Data

Range or hash

Every follower holds the whole database, so replication cannot help when writes or data outgrow one leader. Then you split the data into partitions (shards), the way a booking office sends trains 1 to 999 to one counter and the rest to another. A partition key, such as customer_id, decides each row's partition, and each partition is still replicated. Two rules are common.

Range partitioningHash partitioning
RuleEach partition holds a sorted range of keys, such as A to FPartition = hash(key) mod a fixed number of partitions
Range scansFast: "orders from 1 to 7 March" touches one or two partitionsSlow: neighbouring keys are scattered, so every partition is asked
Load spreadUneven when keys cluster, such as timestampsEven, as long as no single key is hot

Use range for reads in key order and hash for exact-key access. A compound key blends them: hash the first part to pick the partition, then sort by the second inside it, so a customer's orders by time are one read.

Animated sceneHashing customer ids across four shards0:22

Watch for the load on each shard. The router hashes each customer id to pick a shard

Read it as steps

Customers spread evenly across four shards, until one restaurant's order counter turns hot.

Hashing spreads load across keys but cannot split one key. A write-hot key like this counter is split into sub-keys added up at read time, as the sharded counters chapter shows. A read-hot key, such as a famous restaurant's menu, needs a cache or more replicas instead.

Hot spots and rebalancing

The sale showed one kind of hot spot, a partition that gets far more traffic than the rest. The other common cause is keys that arrive in order: range-partition orders by time, and every new order on final night lands in the last partition while the others idle. Hash the key, or prefix it with a shard number, to spread them.

Growth brings the other chore. Moving data onto new nodes is called rebalancing, and the placement rule decides how much moves.

ApproachHow it worksWhen a node is added
hash mod N, with N = nodesA key lives on node hash(key) mod NAlmost every key moves: from 4 to 5 nodes, about 80%
Fixed partitionsMany more partitions than nodes, say 1,000 for 10; whole partitions moveThe new node takes a few from each node; about 1 key in 11 moves
Dynamic splittingA range partition past a size, say 10 GB, splits in twoPartitions move one at a time; suits range keys
Consistent hashingKeys and nodes on a ring; see the key-value store chapterOnly the new node's slice of the ring moves

Fixed partitions keep every move small and predictable; pick a count well above the nodes you expect, because changing it later means splitting every partition.

Why mod N hurts

Hash 9 lives on node 1 of 4, since 9 mod 4 = 1. With a fifth node, 9 mod 5 = 4, so it moves. Only keys whose remainders match both ways stay: 4 in every 20.

Requests find their partition through a routing tier or a client that caches the partition map, kept in a small, strongly consistent coordination service so every router agrees.

Queries that cross partitions

Partitioning makes queries by the partition key fast and every other query awkward. Partition orders by customer_id, and "my orders" hits one partition, but "today's orders for restaurant 77" touches them all. A secondary index on restaurant_id can be built in two ways.

Local indexGlobal index
Where it livesEach partition indexes only its own rowsA separate index, partitioned by restaurant_id
A writeUpdates one partitionUpdates the data partition and an index partition, often a moment later
The dashboard queryAsk every partition and merge: scatter-gatherAsk one index partition, then fetch the rows
Main costEvery query waits for the slowest partitionThe index can lag, and writes do more work

A local index makes the dashboard pay on every read; a global index makes every order pay a little at write time and accepts a moment of lag.

Joins and transactions across partitions are hard for the same reason: several machines must cooperate, and the slowest sets the latency. If restaurants mostly read their own orders and customers theirs, store each order twice, once keyed by each.

Choose the key from the busiest query

List your top three queries with their rates. The partition key serves the busiest from one partition; the rest get an index, a second copy or a slower path.

Part 5 · Choosing a Store

Start from the access patterns

Everything so far comes together in choosing a store. Ask the same five questions every time; each answer narrows the field.

Questions that pick a store
  1. 1What are the main queries?By key, range, many fields, relationships or text
  2. 2Must several rows change together?Yes points to relational
  3. 3What is the read-to-write ratio?Read-heavy: followers and caches. Write-heavy: partitions
  4. 4How big, and how fast growing?Under a few TB and a few thousand writes a second, one leader may do
  5. 5How stale may a read be?A second is fine: async followers. Never: the leader

The food app shows the result: different data, different stores. Keep the number small, because each store is one more thing to run, back up and repair at 2 am.

DataMain access patternStoreWhy
Orders and paymentsCreate, update status, list "my orders"Relational, later partitioned by customerPayment and order change together
Restaurant menusRead a whole menu by restaurant idDocument store with a cacheRead as one unit; fields vary
Rider locationsWrite every few seconds, read the latestIn-memory key-value with expiryOnly the newest value matters
Order eventsAppend; read one order's events in orderWide-column, partitioned by order idHeavy appends, read by key
Dish searchText search that tolerates typosSearch index fed from the menu storeDatabases do typo-tolerant search poorly
Orders and payments

Main access patternCreate, update status, list "my orders"

StoreRelational, later partitioned by customer

WhyPayment and order change together

Restaurant menus

Main access patternRead a whole menu by restaurant id

StoreDocument store with a cache

WhyRead as one unit; fields vary

Rider locations

Main access patternWrite every few seconds, read the latest

StoreIn-memory key-value with expiry

WhyOnly the newest value matters

Order events

Main access patternAppend; read one order's events in order

StoreWide-column, partitioned by order id

WhyHeavy appends, read by key

Dish search

Main access patternText search that tolerates typos

StoreSearch index fed from the menu store

WhyDatabases do typo-tolerant search poorly

Only orders and payments need transactions, so only they pay for a relational primary; the rest use the simplest store for their one pattern.

Default to relational

If nothing above pushes you away, relational is a sound default: transactions, flexible queries and decades of tooling. Leave it only with a reason you can state in one sentence.

In the interview

When a design needs a database, state the access pattern first, then the decision it forces, with a number.

Say
"Reads are about 20 times writes, so followers serve menus and tracking; a customer's own address comes from the leader, because followers can lag a second."
Say
"Orders grow about 7 TB a year, so I partition by customer_id: 'my orders' stays on one partition, and the restaurant dashboard uses a global index that can lag a moment."
Say
"Orders and payments use semi-synchronous replication: every confirmed write is on two machines, for about 1 ms more per write."
Avoid
"NoSQL, because it scales." One relational primary takes thousands of writes a second; name the access pattern that outgrows it.
Avoid
"Shard by hash mod the number of servers." Going from 4 to 5 servers moves about 80% of keys.

Wrap-up

TermMeaning
Write skewTwo transactions check a rule, then write different rows, and together break it
Write-ahead logEvery change appended to disk before it is applied, for recovery and restores
Replication lagHow far an asynchronous follower is behind the leader
FencingRejecting writes from a leader with an older term
Partition keyThe column whose value decides which partition owns a row
Point-in-time recoveryRestoring a snapshot plus the log up to any chosen second

Next: consensus, leader election and coordination, which opens up the voting group and partition-map service this chapter leaned on.

Part 6 · Check yourself

Quick check

Q1Snapshot isolation stops two orders from overwriting one wallet balance, yet a one-time coupon gets used in two tabs at once. Why the difference, and what is the simplest fix?

Think it through, then check.

Q2After a network blip, the old and the new leader both accepted orders for 20 seconds. What went wrong, and which two mechanisms prevent it?

Think it through, then check.

Q31,000 fixed partitions of 20 GB sit on 10 nodes. An 11th node joins and takes in about 100 MB/s. How much data moves, how long does it take, and what if keys were placed by hash mod N?

Think it through, then check.

Q4The food app must never lose a confirmed payment. With one leader and two followers in other zones, should it replicate synchronously, asynchronously or semi-synchronously, and at what cost?

Think it through, then check.

Q5From the abstractions chapter: a "pay ₹450" request times out and the client retries with the same idempotency key, but the first attempt had committed. How does the database make sure the money moves once?

Think it through, then check.

Next chapter · 8 of 39Consensus, Leader Election and Coordination~32 min · 2 animated scenes