System design / Search & storage

Database Reads and Writes for System Design: WAL, B-Trees, LSM Trees, Caching, Replication and Sharding

Why sequential appends beat random writes by 6,000x, and how that shapes the database write path and read path: write-ahead logs, B-Tree vs LSM tree, indexes, cache-aside vs write-through vs write-back, group commit, replication lag, range vs hash sharding, hot keys and CQRS, with the trade-offs to name in an HLD interview.

On this page
  1. Reads and writes are two different jobs
  2. Why sequential beats random: the 6,000x gap
  3. The write path, step by step
  4. Write-ahead logging: the idea you'll see everywhere
  5. The read path, step by step
  6. B-Tree vs LSM tree: update in place, or append and merge
  7. Indexes: faster reads, slower writes
  8. Caching: cache-aside, write-through and write-back
  9. Batching and group commit
  10. Replication: leader-follower, read replicas and lag
  11. Sharding: range, hash and hot keys
  12. CQRS: separate models for writes and reads
  13. Real system: Discord's reads and writes at trillions of messages
  14. When it goes wrong: edge cases and failure modes
  15. How to optimise reads vs writes: the checklist
  16. How to say it in the HLD interview

On the same set of spinning disks, the Kafka team measured sequential writes at about 600MB/sec and random writes at about 100kB/sec. That is a gap of over 6,000x, and nearly every storage design you will meet is a clever way to dodge it. By the end of this blog you will know which tricks speed up reads, which speed up writes, and what each one costs the other side.

Where this fits: Part of the Search & storage series. The core blog follows one query from keyboard to disk. Here we zoom into the storage layer at the bottom: how data is written and read back, then how one database grows into caches, replicas and shards.

Before you start: Helpful, not required: Disk I/O and durability (page cache, fsync) and Storage data structures (B-Tree, LSM, Bloom filter up close). This blog stands on its own.

After reading this you will be able to

  • Trace a write and a read through a database, and say exactly when a commit becomes safe.
  • Explain the write-ahead log and why the same idea shows up in Postgres, Redis, RocksDB and Kafka.
  • Compare B-Trees and LSM trees, and pick one for a read-heavy or write-heavy workload.
  • Choose between cache-aside, write-through and write-back, and name what each one can lose.
  • Explain replication lag, range vs hash sharding, hot keys and CQRS without hand-waving.
  • Answer "how would you optimise reads vs writes?" in an HLD interview, with the cost of every move.

For this blog, we'll focus on a single database first and then scale it out: one machine's read and write paths, then caches, replicas and shards. We'll leave search indexes to How search engines work. If you want the wider database picture (families, transactions, isolation), the Databases for System Design Interviews guide goes deeper.

01Reads and writes are two different jobs

Picture a library. When a book comes back, the librarian can walk it to its exact shelf. That is a slow write, but anyone can find the book in seconds later. Or the librarian can drop it in the returns cart. That is a fast write, but finding that book is painful until someone sorts the cart. Every storage engine picks a spot between the shelf and the cart.

A few words we'll use all the way through:

  • Read path: every step a read takes, from the request to the bytes coming back.
  • Write path: every step a write takes, from the request to the database saying "committed".
  • Latency: how long one request takes. p99 is the latency of the slowest 1 request in 100.
  • Throughput: how many requests per second the system handles.
  • Read-heavy vs write-heavy: which of the two your workload does more of. A news site is read-heavy; a metrics pipeline is write-heavy.
  • Durability: once the database says "saved", the data survives a crash.
  • Amplification: one logical read or write causing many physical ones on disk.

Why care about p99 and not the average? Because your unluckiest users live there. Amazon's Dynamo paper describes storage SLAs measured at the 99.9th percentile for exactly that reason.

Here is the whole picture we'll unpack.

Fig. 1

The write path and the read path share the disk and little else

The write path and the read path share the disk and little elseA client talks to an app server. Writes (blue) go from the app to the write-ahead log, which is appended and flushed to disk. Reads (green) check the cache first, then on a miss go to the index, which points at the data pages, which are read from disk only if they are not already in memory.Databasewriteappend + fsync1 check cache2 on a miss3 read pageif not in RAMClientApp serverCachehot keys in RAMWrite-aheadlog (WAL)Indexsorted keysData pagesbuffer poolDisklog files and data filesBlue: write path (append to the log)Green: read path (stop as early as you can)
Two paths, one disk. Writes want to append to a log; reads want things sorted, indexed and already in memory.

The 30-second answer

"Writes are cheap when they are sequential appends and expensive when they update data in place. Reads are cheap when the data is already sorted, indexed or cached. Every optimisation for one side moves cost to the other."

02Why sequential beats random: the 6,000x gap

Sequential vs random I/O Definition

Sequential I/O reads or writes the next bytes right after the last ones. Random I/O jumps to a new spot each time. On a spinning disk, every jump pays a seek: the arm physically moves to a new track.

The numbers make the point better than any adjective. These are orders of magnitude, not benchmarks, from Peter Norvig's table and Colin Scott's interactive latency page:

OperationRough latencySource
Main memory (RAM) reference100 nsNorvig, Colin Scott
SSD random read16 µsColin Scott's model
Read 1 MB sequentially from memory250 µsNorvig
Disk seek8 ms (10 ms in Dean's version)Norvig, via Colin Scott
Read 1 MB sequentially from disk20 msNorvig

Look at the last two rows. One seek costs almost half as much as reading a whole megabyte in a straight line. That is why the Kafka design docs report linear writes on six 7200rpm SATA disks at about 600MB/sec but random writes at only about 100kB/sec, "a difference of over 6000X". Kafka leans on this, and on the OS page cache: on a 32 GB machine it can use 28-30 GB of cache without holding it in the JVM.

The takeaway: appending to a log is the cheapest write you can make. Hold on to that, because the log shows up in almost every section below.

Seen this before?

Sequential vs random I/O, SSDs vs spinning disks and the page cache are the heart of Disk I/O and durability.

03The write path, step by step

Before you read on

Guess: when Postgres says COMMIT succeeded, has your row reached its table file on disk?

Here is what a Postgres-style database does with an INSERT:

  1. The client sends the INSERT.
  2. The database writes a log record describing the change into the WAL buffer (a small area in RAM for log records).
  3. It changes the table page in the buffer pool, the database's own RAM cache of disk pages. The page is now dirty: newer in memory than on disk.
  4. On COMMIT, the WAL is flushed to disk with fsync, the system call that asks the OS to really put bytes on the device.
  5. Only now does the client hear "committed".
  6. Later, a checkpoint writes dirty pages to the data files in the background.

The sequence below shows when the client gets its OK.

Fig. 2

A Postgres-style write: the ack waits for the log, not the table file

A Postgres-style write: the ack waits for the log, not the table fileSequence. The client sends INSERT. The database writes a log record into the WAL buffer and changes the table page in the buffer pool, marking it dirty. On COMMIT the WAL buffer is flushed to the WAL file on disk with fsync, and only then does the client get OK. Much later a checkpoint writes the dirty page to the data files.1INSERT2log record3change page, mark it dirty4COMMIT5flush + fsync6OK7checkpoint, laterClientDatabaseWAL bufferWAL on diskBuffer poolData filesThe client hears OK once the log is on disk. The table file catches up later.
The ack comes after step 5, not step 7. The log hits disk first; the table file is touched much later.

So the answer to the guess is no. Only the log is guaranteed to be on disk. That is safe, because after a crash the database replays the log and redoes any committed change the data files missed. The Postgres WAL docs spell out why this is fast: the commit costs one sequential append to the log, while the scattered page writes happen later and get combined.

Seen this before?

"write() returned" is not the same as "on disk". Why that gap exists, and what fsync really promises, is in Disk I/O and durability.

04Write-ahead logging: the idea you'll see everywhere

Write-ahead log (WAL) Definition

An append-only file of changes that must reach disk before the change is applied to the main data. After a crash, you replay the log to redo committed work. The rule in one line: log first, flush the log, then touch the data pages.

Think of it as a shop's daybook. The shopkeeper scribbles every sale in a notebook as it happens, and updates the neat ledger at closing time. If the ledger gets coffee on it, the notebook rebuilds it.

Once you see the pattern, you see it everywhere:

  • SQLite has a WAL mode where readers don't block writers and I/O is more sequential. The docs are honest about the cost: for read-mostly apps it "might be very slightly slower (perhaps 1% or 2% slower)".
  • Redis has the AOF, an append-only file of write commands it replays on startup (Redis persistence).
  • Bigtable writes every change to a commit log of redo records before the in-memory table (Bigtable paper, section 5.3).
  • RocksDB puts an optional WAL in front of its memtable (RocksDB Overview).
  • Kafka takes the idea to its end: the log is the product.

Seen this before?

The ext4 journal is a write-ahead log for the filesystem (Disk I/O and durability). Elasticsearch's translog is one too; Elasticsearch from the Top Down calls it a write-ahead log in so many words. We meet it again in Elasticsearch.

05The read path, step by step

Now the other direction. A read for one user's profile goes roughly like this:

  1. The app checks its cache. A cache hit means the answer is there; a cache miss means it isn't.
  2. On a miss, the query goes to the database.
  3. The query planner picks an index (a sorted structure that points at rows) or a full table scan (reading every row).
  4. The index lookup finds where the row lives.
  5. The database checks the buffer pool for that page.
  6. On a buffer miss, it reads the page from disk. The OS page cache may still save you a device read.
  7. The row comes back, and the app fills its cache for next time.

Most systems have hot data: a small set of rows read far more than the rest. The whole read path is designed to keep that set as high up the stack as possible. Why? Do the arithmetic with the table above. RAM at 100 ns vs an SSD random read at 16 µs is roughly 160x. An SSD read vs a disk seek at 8 ms is roughly 500x. (Those ratios are our own division, not measurements.) Most read tuning is about stopping earlier.

06B-Tree vs LSM tree: update in place, or append and merge

The shelf-or-cart choice from the library becomes a real data structure here.

B-Tree Definition

A sorted tree of fixed-size pages, updated in place. A read hops from the root page down to a leaf in a few steps. A write finds the right leaf and rewrites that page, which is random I/O.

The B-Tree is the default index type in Postgres, and a whole SQLite database file is a set of B-tree pages. Your phone is full of them.

LSM tree (log-structured merge tree) Definition

Writes go to a sorted in-memory table called the memtable (plus a WAL). When it fills, it is flushed to disk as an immutable sorted file, an SSTable. Background compaction merges small files into fewer, bigger ones.

That is the RocksDB write path, in the words of the RocksDB Overview. The idea dates to the 1996 LSM-tree paper by O'Neil and colleagues. They wrote it because, for a fast-growing indexed history table, a B-tree "will effectively double the I/O cost". Their fix batches changes in memory and cascades them to disk in a way "reminiscent of merge sort".

The catch is on the read side. A key might live in the memtable or in any of several files. So LSM engines add a Bloom filter per file: a tiny summary that answers "definitely not here" or "maybe here", letting a read skip most files (RocksDB Bloom filter).

Here are both shapes side by side.

Fig. 3

B-Tree updates one sorted copy in place; an LSM tree appends and merges later

B-Tree updates one sorted copy in place; an LSM tree appends and merges laterLeft: a B-Tree with a root page, inner pages and leaf pages. An update rewrites one leaf page in place. Right: an LSM tree. Writes go to a WAL and an in-memory memtable, which flushes to immutable sorted files at level 0. Compaction merges them into fewer, bigger files at level 1. Each file has a Bloom filter so reads can skip files.B-Tree: update in placeLSM tree: append, merge laterflushcompactionRoot pageInner pageInner pageLeaf pageLeaf (rewritten)Leaf pageMemtableRAM, sortedWALappend firstL0: SSTable SSTable SSTableimmutable sorted filesL1: fewer, bigger SSTablesmerged by compactionAn update rewrites that leaf page in placeReads: a few page hops. Writes: random rewrites.Each file carries a Bloom filter to skip itB-Trees pay at write time; LSM trees write fast and pay later by merging
Pay now or pay later. B-Trees keep one sorted copy and rewrite pages; LSM trees append fast and merge in the background.
B-TreeLSM tree
Write costHigher: random page rewritesLower: sequential appends
Point readA few page hopsMemtable plus maybe several files (Bloom filters help)
Range scanStrong: walk sorted leavesMust merge across files
Background workLightCompaction competes for disk
Who uses itPostgres, MySQL InnoDB, SQLiteRocksDB, Cassandra, ScyllaDB, Bigtable

Default choice

Start with a B-Tree database for general OLTP (everyday app transactions). Reach for an LSM engine when writes dominate and you can live with compaction running in the background.

Many candidates say "LSM is faster"

It is faster for writes. Point reads and range scans may touch several files, and compaction steals disk bandwidth from your foreground traffic. Say which side it is faster for.

Seen this before?

Immutable files plus background merging is the LSM idea, covered up close in Storage data structures. Lucene segments and merges work the same way, in How search engines work. The Bloom filter gets its own section in Storage data structures.

07Indexes: faster reads, slower writes

Secondary index Definition

An extra sorted structure on some column (say email) that points back at rows. It turns "scan everything" into "jump straight there".

Indexes are not free. The Postgres docs say it plainly: the system must update every index whenever the table changes. Every index taxes every write.

Uber's post Why Uber Engineering Switched from Postgres to MySQL is a sharp example. In their setup, Postgres secondary indexes pointed at physical row locations, so an update could touch every index on the table, and those extra writes also inflated replication traffic. Treat it as one company's view of its own workload, not a verdict on Postgres.

SetupRead cost on that columnWrite cost
No indexFull scanLowest
One indexFast lookupOne extra structure to update
Five indexesFast on five columnsFive extra structures per write

My rules of thumb: index what you filter and sort on, drop unused indexes, mind column order in composite indexes, and use a covering index (one holding every column the query needs) to skip the table read. Text, location and "similar to this" queries need other index shapes: inverted, geo and vector indexes.

Seen this before?

The inverted index is the backbone of full-text search. It is introduced in Storage data structures and built properly in How search engines work. Even Postgres ships one: GIN stands for Generalized Inverted Index.

08Caching: cache-aside, write-through and write-back

A cache is a fast copy of data that lives somewhere expensive. Think of the snacks on your desk versus the supermarket across town. Four more words:

  • Hit ratio: the share of reads the cache answers.
  • TTL (time to live): how long an entry stays before it expires.
  • Invalidation: removing or updating an entry when the source changes.
  • Stale read: reading an old value the source has already changed.

Cache-aside (also called look-aside or lazy loading) is the common default. The app reads the cache; on a miss it reads the database and fills the cache. On a write, it updates the database and then deletes the cache key so the next read refills it. Facebook's memcache is the classic example: a "demand-filled look-aside cache" that, per Scaling Memcache at Facebook, "handles billions of requests per second and holds trillions of items".

Write-through sends every write to the cache and the database together before the ack. AWS's caching best practices put it as the cache being "updated in real time when the database is updated". Reads after a write are fresh; writes get slower.

Write-back (or write-behind) writes to the cache, acks straight away and flushes to the database later. Writes are fast, but a crash loses whatever hadn't been flushed. You already use one: the OS page cache works this way. Redis offers the same dial for its AOF: with appendfsync everysec, "you can only lose one second worth of writes".

The three patterns differ in one arrow: where the ack happens.

Fig. 4

Three cache write patterns, and where the ack happens

Three cache write patterns, and where the ack happensThree columns, each with an app, a cache and a database. Cache-aside: the app reads the cache, reads the database on a miss and fills the cache; writes update the database and delete the cache key. Write-through: the write goes to the cache and the database before the ack. Write-back: the write goes to the cache, is acknowledged at once and is flushed to the database later.Cache-asideWrite-throughWrite-back1 read2 on miss1 write1 writeAppCacheDBAppCacheDBAppCacheDBSteps1 read the cache2 miss: read DB, fill cachewrite: update DB,then delete the keySteps1 write to the cache2 cache writes the DB3 ack after bothfresh reads, slower writesSteps1 write to the cache2 ack straight away3 flush to DB latercrash can lose the tailAck after the DB writeAck after cache + DBAck after cache onlysynclater
Watch the ack. That one arrow decides how fast writes are and what you can lose.

Hit ratios are often modest. Jeff Dean's WSDM 2009 keynote reports Google's cache servers in front of the index had hit rates "typically 30-60%", with a big latency spike when the cache was flushed. The database behind the cache must still survive a cold start.

Many candidates add a cache and stop

Say how it is invalidated, what happens on a cold start, and what a stale read costs the user. "Redis in front" is not a design until you answer those three.

Seen this before?

The page cache as a write-back cache is in Disk I/O and durability. Every cache layer a query passes through is mapped in the core blog.

09Batching and group commit

Batching turns many small writes into one bigger one. Think of a postman who waits to fill the bag instead of driving each letter over separately. Group commit is the database version: when several transactions commit at once, one fsync of the log makes all of them durable. The Postgres WAL docs describe this, and Bigtable uses group commit too. It is a big reason databases often get more efficient per write as load rises, up to a point.

Some systems let you go further and trade a little safety for speed:

  • Postgres asynchronous commit acks before the WAL flush. A crash can lose the last few transactions, but the docs stress it is "data loss, not data corruption".
  • Redis appendfsync has three settings: always, everysec and no.
  • Apps batch too: bulk inserts, or buffering writes in a queue. Discord indexes messages into Elasticsearch in bulk with refresh set to every 60 minutes, on purpose, because people mostly search older messages.

Default choice

Keep synchronous commit for money and orders. Batch or go async for logs, metrics and anything you can replay from somewhere else.

10Replication: leader-follower, read replicas and lag

A replica is a full copy of the data on another machine. In the common setup, one leader (or primary) takes all writes and one or more followers (standbys, read replicas) copy them. With synchronous replication, the leader waits for a follower before acking; with asynchronous replication, it doesn't. Replication lag is how far behind a follower is. Failover is promoting a follower when the leader dies.

Here is the neat part: in Postgres, the leader ships its WAL to standbys, which replay it. The same log that protects you from crashes now feeds your replicas. The log-shipping docs note this is asynchronous by default.

Read replicas scale reads, not writes. Every replica still applies every write.

Async replication creates a classic bug. Watch what happens when a user edits their profile and refreshes.

Fig. 5

Replication lag: the leader said OK, the follower has not caught up

Replication lag: the leader said OK, the follower has not caught upSequence. A user updates their name. The app writes to the leader, which acknowledges. The app tells the user it is saved. The user refreshes, the app reads from a follower, and the follower returns the old name, because the WAL stream from the leader arrives later.1change my name2UPDATE3committed4saved!5refresh profile6SELECT7old name8WAL arrives, replayedUserAppLeaderFollowerThe gap between the leader's ack and the follower catching up is replication lag
Saved, then gone. The leader acked, but the follower hadn't replayed the change when the read arrived.

This breaks read-your-writes: the promise that you see your own changes. Common fixes: read from the leader for a short window after a user writes, or pin a user's session to the leader or an up-to-date replica.

Not every system has a single leader. Amazon's Dynamo is leaderless and "always writeable": with sloppy quorums and hinted handoff a shopping cart accepts writes even during failures, and Merkle trees repair differences later.

Many candidates say "add read replicas" for a write-heavy system

Replicas copy the writes; they don't share them. For write scale you need batching, a write-friendly engine, or sharding.

Seen this before?

Shipping the WAL is the write-ahead log doing a second job. Search engines replicate too: primary and replica shards in Elasticsearch.

11Sharding: range, hash and hot keys

When one machine can't hold the data or take the writes, you split the data. A shard (or partition) is one slice, living on its own machine. The shard key decides which slice a row goes to. Think of a post office sorting letters into bins by PIN code.

  • Range sharding splits keys by ranges. Neighbours stay together, so range scans are cheap. Bigtable works this way: "each row range is called a tablet" (Bigtable). The risk is a hot tail: if keys grow over time, all new writes land on the last range.
  • Hash sharding hashes the key to pick a shard. Load spreads evenly, but ordering is lost.
  • Consistent hashing places shards on a ring so adding a node moves only a slice of the keys, not all of them (Dynamo).
  • A hot key or hot partition gets far more traffic than the rest. A cross-shard query needs data from more than one shard.

Compare the two main strategies and where each one runs hot.

Fig. 6

Range sharding vs hash sharding

Range sharding vs hash shardingLeft: range sharding by user id. Shard 1 holds ids 1 to 1M, shard 2 holds 1M to 2M and shard 3 holds 2M and up, so every new signup lands on shard 3. Right: hash sharding. A hash of the key spreads keys evenly over three shards, but one very popular key still overloads the shard that owns it.Range shardingHash shardingShard 1user_id 1 to 1MShard 2user_id 1M to 2MShard 3user_id 2M and uphash(key) mod NShard 1even shareShard 2even shareShard 3hot keyall new signupsGood: neighbours together, cheap range scansRisk: the newest range runs hotone celebrity key = one sad shardGood: load spreads evenlyRisk: no cheap range scans, hot keysPick the shard key from the query you run most
Both can run hot. Range sharding piles up at the end; hash sharding spreads load, but one popular key can still sink a shard.

Discord's How Discord Stores Trillions of Messages describes hot partitions on Cassandra causing cascading latency. Part of the fix was a layer of Rust data services that coalesce identical reads and route requests by channel ID with consistent hashing.

Choosing the shard key is the real decision. Figma's database team, whose stack grew "almost 100x since 2020", first split tables into separate databases (vertical partitioning), then sharded horizontally. They grouped tables sharing a shard key into "colos" and kept logical shards separate from physical machines. The price: cross-shard queries became limited. Instagram's Sharding & IDs post covers another wrinkle: generating unique IDs across Postgres shards.

If a query doesn't include the shard key, you must ask every shard and merge the answers. That is scatter-gather. Dean's WSDM keynote describes Google's index servers as sharded by document, so every shard processes every query.

Seen this before?

Scatter-gather is how a search query fans out, in the core blog. Elasticsearch's query_then_fetch does the same thing across shards (Elasticsearch from the Top Down), covered in Elasticsearch.

12CQRS: separate models for writes and reads

CQRS Definition

Command Query Responsibility Segregation: one model handles updates (commands), a different model serves reads (queries), and the two are kept in sync, often through a log or events.

You probably run a version of this already. Postgres is the source of truth, and an Elasticsearch index serves search. A search index is a read model. DoorDash goes further in its in-house search engine, splitting a non-replicated indexer from replicated searchers.

The honest caveat comes from Martin Fowler: for most systems, CQRS "adds risky complexity". The read side is always a little behind, so you are buying read speed with eventual consistency. Don't start here. Reach for it when the shape of your reads and writes really diverges.

13Real system: Discord's reads and writes at trillions of messages

Discord's storage story ties most of this blog together. It ran 12 Cassandra nodes in 2017 and 177 by early 2022, holding trillions of messages. After migrating, it ran 72 ScyllaDB nodes. The before and after:

Metric (p99)CassandraScyllaDB
Fetch historical messages40-125 ms15 ms
Insert a message5-70 mssteady 5 ms

Map the fixes to this blog: request coalescing tamed hot keys (caching), channel-ID routing used consistent hashing (sharding), both databases are LSM-based (B-Tree vs LSM), and the move had no downtime, at the cost of a big migration project.

14When it goes wrong: edge cases and failure modes

  • Cache stampede. A hot key expires and thousands of misses hit the database at once. Coalesce identical requests (as Discord did) or refresh popular entries before they expire.
  • Lost writes on failover. With async replication, the leader can ack a write, die and be replaced by a follower that never got it.
  • Compaction debt. In an LSM, background merges compete with live traffic. Merging is also write amplification: McCandless measured Lucene merging copy 67.30 GB to build a 10.87 GB index, a factor of 6.19 (Visualizing Lucene's segment merges).
  • Index bloat. Update-heavy tables with many indexes pay for every index on every write, and then again in replication (Uber).
  • Hot shard. A celebrity key overloads one shard. Rebalancing moves data around; consistent hashing limits how much moves.

15How to optimise reads vs writes: the checklist

Cheapest rung first. Every rung costs the other side something.

Optimise readsCosts the other side byOptimise writesCosts the other side by
Add the right index, drop unused onesSlower writes per indexAppend, don't update in place (WAL, LSM)Slower point reads, compaction
Cache-aside with TTL and invalidationStale reads, invalidation workBatch and group commitA little added latency per write
Read replicasReplication lagAsync commit or everysecA small window of possible loss
Denormalise or precomputeMore data to update per writeFewer indexesSlower reads on those columns
CQRS read model or search indexEventual consistency, more moving partsQueue or write-back buffer in frontReads may miss queued writes
Shard by the key you read withCross-shard writes get harderShard by a high-cardinality keyScatter-gather for other queries
Bloom filters on LSM lookupsMemory per fileBulk load with indexes or refresh offData not readable until done

Whiteboard version

  • Know the read/write ratio first.
  • Writes: append, batch, fewer indexes.
  • Reads: index, cache, replicate.
  • Replicas scale reads; shards scale writes.
  • Say where the ack happens.
  • Name the cost of every move.

16How to say it in the HLD interview

When I ask about storage, I'm listening for three things. Do you know your read/write ratio? Do you know where the ack happens? Can you name what each optimisation costs? Here's a talk track that hits all three.

Interview talk track

"Let me estimate the split first: about 100 reads per write, so this is read-heavy. I'll start with Postgres as the source of truth, because B-Trees give good reads and we need transactions. Hot reads go through a cache-aside layer with a short TTL, and I delete the key on every write. For read scale I'll add replicas, and to avoid users missing their own edits I'll read from the leader for a few seconds after a write. If writes outgrow one leader, I'll shard by user ID with consistent hashing, and for celebrity keys I'll coalesce reads and cache them."

Many candidates lose points in the same places: using replicas for write scaling, adding a cache with no invalidation story, sharding before trying indexes, caching and replicas, and picking a shard key that forces scatter-gather on the main query.

What if writes are now 10x the reads?

Move toward an LSM-based store, batch writes, and put a queue in front to absorb bursts. Cut indexes you don't need. Accept that point reads get a little slower.

What if a celebrity key melts one shard?

Cache that key aggressively, coalesce identical reads into one database call, and if writes are the problem, split the key into sub-keys and merge on read.

A user doesn't see their own update. Why?

Replication lag. The write went to the leader, the read hit a follower that hadn't replayed it yet. Read from the leader for a short window after a write, or route the session to an up-to-date replica.

What happens if the cache dies?

Every request becomes a miss, and the database takes the full load at once. Plan for it: warm the cache gradually, coalesce misses, and size the database so a cold start degrades service instead of killing it.

Do we need CQRS here?

Only if reads and writes really want different shapes, like a transactional store plus full-text search. Otherwise it adds sync lag and moving parts for little gain.

Key takeaways

  1. Sequential appends are cheap and random updates are expensive. That 6,000x gap in the Kafka docs shapes the WAL, LSM trees and batching.
  2. A commit is durable when the log is on disk, not when the table is. The same log idea runs Postgres, Redis, RocksDB, Kafka and ext4.
  3. B-Trees favour reads, LSM trees favour writes. Indexes and caches speed up reads and tax writes.
  4. Replicas scale reads, not writes, and bring lag. Shards scale writes, and bring hot keys and scatter-gather.
  5. In the interview, always say what each optimisation costs the other side.

Next up

How search engines work: now that you know how bytes are stored, see how text becomes searchable.

Related

Real-world story

Discord: how it moved trillions of messages from 177 Cassandra nodes to 72 ScyllaDB nodes, and how it indexes them for search.

Further reading

Interview practice