#Choosing a database in an interview
AddedIn a design interview the database question is rarely "SQL or NoSQL". It's "which questions must be cheap, how often are they asked, and how much data is there?" A candidate who names a product first ("we'll use Cassandra") and justifies it afterwards sounds like they're guessing. A candidate who lists the reads and writes, puts numbers on them and then picks the store that answers those questions cheaply sounds like an engineer, even if they pick the same product.
A useful mental model: every database is an index somebody already built for you. A B-tree in PostgreSQL makes "rows with this key, or this range of keys" cheap, and its transactions make "change these five rows together" safe. A hash-partitioned key-value store makes "get this key" cheap at almost any scale and makes everything else your problem. An inverted index makes "documents containing these words" cheap. Object storage makes "store and serve this 3 MB file" cheap. Choosing a database is choosing which questions get the precomputed answer, so you have to know the questions first.
So the order in the interview is always the same: access patterns, then numbers, then the store. Write the access patterns as short lines on the board before any boxes appear:
| Access pattern | Shape | How often |
|---|---|---|
| Place an order (order + items + stock change) | multi-row write, must be atomic | 14/s at peak |
| Show a customer's recent orders | key + range (customer, newest first) | 1,400/s at peak |
| Search products by text and filters | full-text + facets | 500/s |
| Store and serve product photos | large immutable blobs | 2,000 GETs/s, mostly from a CDN |
| Revenue per region per day | scan + aggregate over history | a few times an hour |
Each line points at a kind of store: atomic multi-row writes point at a relational database, text search at an inverted index, blobs at object storage, big aggregates at a column store or warehouse fed from the main database.
A short decision path
Once the access patterns are on the board, a few questions settle the choice. Ask them in this order, because the earlier ones are harder to change later:
| Ask | If the answer is… | Lean towards |
|---|---|---|
| Must several records change together, correctly (money, stock, bookings)? | yes | a relational database (PostgreSQL, MySQL), or distributed SQL if it must span regions |
| Does it fit on one primary at peak, with headroom for 2–3 years? | yes | that relational primary plus read replicas and a cache; stop here |
| no, because of writes or size | a natively partitioned store (Cassandra/ScyllaDB, DynamoDB, MongoDB sharded) or a sharded SQL layer (Vitess, Citus) | |
| Are the reads by key or key range, known in advance? | yes | key-value or wide-column, designed around those keys |
| ad hoc, many joins, changing questions | relational | |
| Is it text relevance, filters and facets? | yes | a search index next to the source of truth |
| Is it large files? | yes | object storage, metadata in the database |
| Is it time-ordered measurements read as aggregates? | yes | a time-series store |
| Is it many-hop relationships (friends of friends, fraud rings)? | yes | a graph store, or precomputed results |
| Is it big scans and aggregates over history? | yes | a column store or warehouse fed by CDC |
The Databases course has the deeper version of this guide, with real query timings; in the interview, the short version plus the numbers is what you need. The second question, "does it fit on one primary?", is the one candidates skip, and it's the one the numbers answer:
// Does one relational primary (plus read replicas) carry this workload, or do the numbers force something else?
const DAY = 86_400;
// Rules of thumb for one well-tuned primary on a large instance (not measurements; tune for your hardware).
const ONE_PRIMARY = { writesPerSec: 5_000, dataTB: 5 };
// peak: busiest hour as a multiple of the daily average (3 for human traffic; sensors report at a steady rate).
const scenarios = [
{ name: "shop orders", dau: 2e6, writesPerUser: 0.2, readsPerUser: 20, rowBytes: 1_000, years: 5, peak: 3 },
{ name: "chat messages", dau: 50e6, writesPerUser: 40, readsPerUser: 200, rowBytes: 300, years: 5, peak: 3 },
{ name: "IoT metrics", devices: 1e6, samplesPerDevicePerDay: DAY / 10, readsPerDay: 2e6, rowBytes: 50, years: 1, peak: 1 },
];
const fmt = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : n.toFixed(0));
for (const s of scenarios) {
const writesPerDay = s.dau ? s.dau * s.writesPerUser : s.devices * s.samplesPerDevicePerDay;
const readsPerDay = s.dau ? s.dau * s.readsPerUser : s.readsPerDay;
const peakWrites = (writesPerDay / DAY) * s.peak;
const peakReads = (readsPerDay / DAY) * s.peak;
const dataTB = (writesPerDay * 365 * s.years * s.rowBytes) / 1e12;
const problems = [];
if (peakWrites > ONE_PRIMARY.writesPerSec) problems.push("write rate");
if (dataTB > ONE_PRIMARY.dataTB) problems.push("data size");
const verdict = problems.length ? `outgrows one primary (${problems.join(", ")})` : "fits one primary + replicas";
console.log(`${s.name}: peak ${fmt(peakWrites)} writes/s, ${fmt(peakReads)} reads/s, ${dataTB.toFixed(2)} TB after ${s.years} y`);
console.log(` -> ${verdict}`);
}shop orders: peak 14 writes/s, 1.4k reads/s, 0.73 TB after 5 y
-> fits one primary + replicas
chat messages: peak 69.4k writes/s, 347.2k reads/s, 1095.00 TB after 5 y
-> outgrows one primary (write rate, data size)
IoT metrics: peak 100.0k writes/s, 23 reads/s, 157.68 TB after 1 y
-> outgrows one primary (write rate, data size)- Each scenario turns users and behaviour into the three numbers a database cares about: peak writes per second, peak reads per second and total bytes.
writesPerDay / 86,400is the average rate; multiplying bypeak(3 for human traffic, 1 for sensors that report on a fixed clock) gives the busiest hour. - The shop: 2M daily users placing 0.2 orders each is 400,000 orders a day, about 4.6 per second on average and 14 per second at peak. Five years of 1 KB rows is 0.73 TB. One relational primary handles that with room to spare, and read replicas absorb the 1,400 reads/s. This is the honest answer for most business systems, and saying it out loud scores points: you aren't sharding a database that fits on one machine.
- Chat: 50M users sending 40 messages each is 2 billion messages a day, 69,400 writes/s at peak, and 1,095 TB over five years. That outgrows one primary on both counts by an order of magnitude, so the design needs a store that partitions writes across many machines (a wide-column store such as Cassandra or ScyllaDB, or a sharded relational setup), and the conversation moves to the partition key.
- IoT metrics: 1M devices reporting every 10 s is a steady 100,000 writes/s of tiny rows, almost never read individually, 158 TB a year. The shape (append-only, time-ordered, read as aggregates) points at a time-series store with compression and downsampling, not a general-purpose database. See Time-series, wide-column and graph stores.
ONE_PRIMARYholds rules of thumb, not measurements: a well-tuned PostgreSQL or MySQL primary on a large instance comfortably takes thousands of simple writes per second and a few terabytes. Real limits depend on row size, indexes, transactions and hardware, so say "roughly" and move on; the verdict only matters when you're within a factor of two of the line, and then you'd benchmark.- The point of the model: the store falls out of the numbers and the shapes. If you changed the chat scenario to 500k daily users, it would drop to about 700 writes/s, which one PostgreSQL primary handles easily. Its 11 TB over five years is still past the model's 5 TB line, so the plan would be to archive old messages or partition by conversation in a year or two, not to shard on day one.
Real systems usually end up with several stores, each holding the data in the shape one access pattern needs. For an e-commerce site:
| Data | Store | Why this one |
|---|---|---|
| Orders, payments, stock, customers | PostgreSQL (source of truth) | transactions, constraints, ad hoc queries |
| Carts, sessions, rate-limit counters | Redis | sub-millisecond key access; losing some is tolerable |
| Product search | Elasticsearch / OpenSearch | inverted index, relevance, facets |
| Product photos | object storage + CDN | cheap, durable, served at the edge |
| Analytics events and reports | a column store or warehouse | scans over billions of rows |
The cost is that every extra store is a copy you must keep in sync and a system you must operate. Name the source of truth for each piece of data and how the copies are fed: change data capture or a transactional outbox, not "the service writes to both" (see Distributed transactions: two-phase commit, sagas and the outbox). And say how stale each copy may be: search results a second behind are fine; a stock count a second behind may oversell.
A chat product has 300,000 daily users who each send 30 messages a day. Messages are always read by conversation, newest first. The interviewer asks whether you'd use PostgreSQL or Cassandra. What do you say?
Show answer
PostgreSQL is fine at this size. The numbers decide it, not the word "chat".
- Writes: 300,000 × 30 = 9 million messages a day, about 104 per second on average and roughly 310 per second at a 3× peak. That's far below what one relational primary handles.
- Storage: at 300 bytes a message, 9M × 300 B × 365 is about 1 TB a year. Five years is 5 TB, near the edge of the rule of thumb, so you'd plan for partitioning by conversation or archiving old messages to cheaper storage, but not on day one.
- The access pattern, "messages in conversation X, newest first", is an index on
(conversation_id, created_at); one B-tree range read per screen. - Cassandra's strengths (spreading 70k writes/s over many nodes, multi-datacentre replication) solve a problem this product doesn't have yet, and its costs (no joins, queries limited to the key design, more operations work) are paid from day one.
- The answer that scores: "PostgreSQL now, with
conversation_idas the future partition key, so if we grow 50× we can shard by conversation or move messages to a wide-column store without changing how the app reads them."