System Design Interview Questions

September 27, 202616 min readUpdated 8/22/2026

The questions you should be able to answer out loud, with answers short enough to actually say. Each one links to the post that works it through properly, and most are followed by the follow-up an interviewer asks next — because the follow-up is usually where the real assessment happens.

This is the last post in the System Design track and it assumes the other seventeen. If an answer here feels compressed, that is deliberate: it is meant to be the version you say in thirty seconds, not the version you understand.

The approach

How do you approach a system design question?

Four steps, and keeping the first three short is what leaves room for the fourth.

   1  SCOPE      5-10 min   functional + non-functional requirements
   2  ESTIMATE   5 min      QPS, storage, bandwidth
   3  HIGH LEVEL 15-20 min  boxes, arrows, API, schema
   4  DEEP DIVE  15-20 min  one hard part, usually the one THEY pick

Follow-up: “What are you optimising for?” Say which non-functional requirement dominates and why the design bends around it. See where to start.

What is the interviewer actually scoring?

Not whether your design matches a model answer — there isn’t one. They are checking whether you ask before building, can estimate, know what each standard component is for, can name the trade you just made, know where it breaks, and can go deep when pushed.

What is the most common mistake?

Drawing before asking. The second most common is designing for Google — sharding and multi-region replication for a system with ten thousand users. Over-engineering reads as inexperience, not ambition.

How do you estimate?

Users × actions/day ÷ 100,000 = per second. Then × read:write ratio, × bytes per record, ×3 for peak. Round 86,400 seconds to 100,000 and say you are rounding.

Follow-up: “What does that number tell you?” That is the actual question. “30,000 reads/sec, so the read path is the entire design” is the answer; the arithmetic was just how you got there. See estimation.

Fundamentals

Vertical or horizontal scaling?

Vertical is a bigger machine: no code changes, hard ceiling, still one machine to lose. Horizontal is more machines: no ceiling, gives redundancy, and requires the work to be divisible — statelessness for app servers, sharding for databases.

   VERTICAL  [ 8 cpu ] -> [ 64 cpu ]     no code changes, hard ceiling
   HORIZONTAL  [ x ]   -> [x][x][x][x]   no ceiling, needs divisible work

In practice: horizontal for the app tier, vertical first for the database. Reversing that — sharding a database that would fit on one larger box — is the most expensive premature decision in this field.

What does “stateless” actually require?

That any server can handle any request. So: no server-side sessions (use signed tokens), no uploads on local disk (object storage), no scheduled jobs in the app process (ten servers means the nightly job runs ten times), no in-memory rate-limit counters (the limit multiplies by the number of servers).

   login   ──> [ app 1 ]   sessions = {abc123: user 7}
   next    ──> [ app 2 ]   sessions = {}          -> "please log in"

   fix: put the session IN the request (a signed token),
        so no server has to remember anything.

Follow-up: “What does a signed token cost you?” Revocation. Nothing is looked up, so a banned user’s token works until it expires. Mitigate with short lifetimes, or re-read the user row anyway. See load balancing.

Layer 4 or layer 7 load balancing?

Layer 4 forwards packets: fast, and it cannot route by path or health-check meaningfully. Layer 7 reads the HTTP request: path routing, TLS termination, safe retries, and a health check that asks “does this instance work?” rather than “is the port open?”

   L4   is the port open?          a deadlocked process passes this
   L7   does GET /health return    it does not pass this
        200 with its dependencies
        reported separately?

For an HTTP API, layer 7, and the health check row is why.

What makes a good health check?

It reports dependencies separately and lets only the ones you genuinely cannot serve without decide whether traffic arrives. If a cache outage marks every instance unhealthy, the load balancer pulls the whole fleet and a slow site becomes no site.

Also: liveness restarts, readiness diverts. Never let a readiness failure trigger restarts, or a database outage restarts your entire fleet in a loop.

SQL or NoSQL?

Start relational unless you can name the specific reason not to. Money, bookings, orders and inventory all involve invariants across rows, and a transaction is the cheapest way to enforce one. Reach for non-relational when the access pattern is genuinely one key at a time at enormous volume — sessions, event logs, feed caches, telemetry.

Caching

Which caching pattern, and why?

   READ                          WRITE
     look in cache                 write to database
       hit  -> return              DELETE the cache entry
       miss -> read database          (delete, not update — two
               store in cache          serialisation paths drift)
               return

Cache-aside: the reader fills on a miss, the writer deletes. Deleting rather than updating means the two serialisation paths cannot drift; the worst case is an extra database read.

What should you cache?

Reads that are hot, expensive, and rarely written — all three. Never cache anything where stale means wrong: balances, remaining inventory, permission checks, booking availability.

How do you invalidate?

Both explicit deletion and a TTL. Explicit alone means one forgotten write path caches a wrong value forever; TTL alone means everything is stale for its whole lifetime. Together, each covers the other’s failure.

Follow-up: “How do you make sure no write path forgets?” Hang invalidation off a method every write path already calls, ideally one whose omission also breaks something loud.

What is a cache stampede?

A popular key expires and every concurrent request misses at once, so the database gets the full unfiltered load of your hottest item. Fixes: jittered TTLs, a lock so one request recomputes, or background refresh before expiry.

   popular key expires at t=0

   t=0.000  request 1   -> miss -> query the database
   t=0.001  request 2   -> miss -> query the database
   ...                                the first query has not
   t=0.050  request 500 -> miss ->    returned, so nothing is cached

The related operational rule: never flush a cache on a running system. A cold cache at a 95% hit rate is a twenty-fold load increase.

What happens when the cache goes down?

Nothing, if it was built correctly — every call returns “miss” rather than raising, with a short timeout so a hung cache cannot park every worker on a socket read. The claim only counts if the test suite runs with it stopped. See caching.

Databases

How would you scale a database?

In order, and stop as soon as it is enough:

   indexes + query fixes    solves ~70% of "the database is slow"
   + connection pooling     ~80%
   + a bigger machine       ~90%
   + read replicas          ~97%
   + partitioning           ~99%
   + sharding               the remaining 1%

How do you design an index?

For the query you actually run: equality columns first, then the range or sort column. Then check the plan — and delete redundant indexes, since a B-tree on (a, b) already answers everything an index on (a) would.

What is the most common database performance bug?

N+1 queries, and it is invisible in development because there are twelve rows. Each extra query is a network round trip, which costs more than reading a megabyte from memory.

What do you lose by sharding?

Cross-shard joins, cross-shard transactions, simple auto-increment ids, and any query that doesn’t carry the shard key. Operations multiply by N.

   shard by property_id   "this property's calendar" -> 1 shard
                          "my bookings"              -> ALL shards
   shard by guest_id      the reverse

   You are choosing which query gets to be fast.

Follow-up: “How do you pick the key?” Ask which query must be fast, because you are choosing which one gets to be. And beware hot shards — even key distribution is not even load distribution.

What is replication lag and why does it matter?

Replicas are asynchronous, so they are behind. A user updates their profile, the next read hits a lagging replica, and their change appears to vanish — a bug they report and you cannot reproduce. Route a user’s reads to the primary briefly after they write. See scaling the database.

Distributed systems

Explain CAP.

During a network partition you must choose availability or consistency. Partition tolerance is not optional — networks fail — so the real question is what you do when nodes cannot talk: refuse the write and stay correct, or accept it and diverge.

   [ node A ] ── ✗ ── [ node B ]     a write arrives at A,
                                      B cannot be told

   CP: refuse it.  Correct, and this node is DOWN.
   AP: accept it.  Up, and the two now DISAGREE.

Follow-up: “And when there is no partition?” PACELC: else, choose latency or consistency. That is the trade you live with daily, since partitions are rare and every consistency guarantee is bought with waiting.

Which consistency model would you choose?

   is this room available?   STRONG     stale = a double booking
   charging a card           STRONG     money
   search results            eventual   a second late is invisible
   your own profile edit     read-your-writes
   review count              eventual   nobody counts 127 vs 128

It is a property of the operation, not the system. Availability checks and payments need strong; search results and review counts are fine eventually consistent; a user’s own edits need read-your-writes.

What is the dual-write problem?

Two datastores cannot be updated atomically — no transaction spans a database and a search index, or a database and an email provider. Commit, then update the second, and a crash between leaves them divergent.

Follow-up: “So how do you fix it?” Say which store is the truth and make every other copy rebuildable. Then close the gap with a transactional outbox, or change data capture. See consistency and CAP.

Why can’t you have exactly-once delivery?

The two generals problem: when a call times out you cannot tell whether it failed or succeeded with a lost response. What exists is at-least-once delivery plus idempotent handling, which produces exactly-once effects — and that is what you actually want.

What is a transactional outbox?

Write the intent to send as a row in the same transaction as the business change, so both commit or neither does. A separate worker reads the table and does the work, retrying until it succeeds. The rule: the enqueue must never commit — the caller’s commit is what makes it atomic.

How does a worker claim work without duplicating it?

   FOR UPDATE              A: rows 1-20    B: BLOCKS      -> 1x throughput
   FOR UPDATE SKIP LOCKED  A: rows 1-20    B: rows 21-40  -> 2x, no coordination

SELECT ... FOR UPDATE SKIP LOCKED. FOR UPDATE stops two workers taking the same rows; SKIP LOCKED stops the second worker waiting for the first, which is what makes additional workers add throughput rather than queue behind each other.

Design a retry policy.

Exponential backoff, capped, with jitter, and a limit on attempts after which the message is dead-lettered. Growth stops retries becoming the load; the cap stops a recoverable message waiting a week; jitter stops every client retrying in unison; the attempt limit is the difference between a queue and a spin loop.

   BEGIN
     INSERT INTO bookings ...     the business change
     INSERT INTO outbox   ...     "and an email needs sending"
   COMMIT                         both, or neither

   backoff: 2, 4, 8, 16, 32, 64, 128, 256 seconds, then DEAD

Follow-up: “What do you do with dead letters?” Alert on the count. A DLQ nobody monitors is a folder where data goes to be forgotten. See message queues.

Concurrency

Two users book the last room simultaneously. What happens?

With check-then-write, both succeed — the gap between the check and the insert is the bug, and a transaction does not close it at the default isolation level, because that is a phantom read. The fix is a database constraint that makes the overlap unrepresentable.

   guest A:  is it free? ─> YES ──────────> INSERT ✓
   guest B:      is it free? ─> YES ──────────> INSERT ✓
                            ▲
        both checks ran before either insert landed
ALTER TABLE bookings ADD CONSTRAINT bookings_no_overlapping_bookings
EXCLUDE USING gist (
    property_id WITH =,
    daterange(check_in, check_out, '[)') WITH &&
) WHERE (status IN ('PENDING', 'CONFIRMED', 'COMPLETED'));

Follow-up: “So why keep the application check?” For the error message. It handles the common case where the dates were already taken; the constraint handles the race. Say explicitly which one is the guard.

Optimistic or pessimistic locking?

Optimistic when conflicts are rare: no waiting, and the caller retries on the rare miss. Pessimistic when contention is high, because under real contention everybody retrying and failing is worse than queuing politely.

Better than either, when it applies: a constraint. There is no lease to expire and no lock to hold.

What is wrong with a distributed lock?

It is a lease with a timeout, and timeouts can be wrong. A holder that pauses — garbage collection, a descheduled VM — can have its lock expire while it still believes it holds it, and then two processes are in the critical section with nothing detecting it. Use one only when there is no datastore that owns the resource.

How do you make an operation safe to retry?

An idempotency key, stored with a unique constraint and the original response, so a retry receives the same answer rather than a conflict. Or make the operation naturally idempotent — writing a document whose id is the entity’s id is the same whether it runs once or five times.

Never read-modify-write a counter; use an atomic increment. See concurrency and locking.

Rate limiting and ids

Which rate limiting algorithm?

   fixed window     100 at 11:59:59 + 100 at 12:00:00 = 200 in one second
   sliding log      exact, and stores one timestamp per request
   sliding counter  two counters, interpolated — approximate
   token bucket     two numbers, burst set independently of rate  ✓

Token bucket: two numbers of state regardless of traffic, and burst is a parameter you set independently of the rate. Never fixed window for anything security-related — a client straddling the boundary gets double the limit in one second.

Why must the counter be atomic?

Read-modify-write loses updates under concurrency, so the effective limit multiplies by the number of workers — and it passes every sequential test, because sequentially it is correct. Use a single atomic command, or a script the datastore executes indivisibly.

tokens = redis.get(key)            # process A reads 1.0    process B reads 1.0
if tokens >= 1: tokens -= 1        # A: allow               B: allow
redis.set(key, tokens)             # A writes 0.0           B writes 0.0

Follow-up: “How would you test that?” Concurrently, with an exact assertion. Fifty threads against a twenty-token bucket must allow exactly twenty; a racy implementation allows twenty-three.

Who do you rate limit?

The account when there is one, the IP otherwise. Never trust X-Forwarded-For unless a proxy you control set it — it is client-supplied, so honouring it blindly lets anyone mint a fresh bucket per request. And never key login on the submitted email, or an attacker can lock a victim out of their own account.

Fail open or fail closed?

For availability protection, open — a cache outage must not become a login outage. For quota enforcement someone is billed against, closed. Decide deliberately and check that no code path escapes the decision. See rate limiting.

How do you generate ids across many machines?

The trade is sortability against coordination. Auto-increment is small and sorted and breaks when sharded. UUIDv4 needs no coordination and hurts B-tree inserts because it is random; UUIDv7 fixes that with a timestamp prefix. Snowflake gives 64-bit time-sortable ids with no coordination — and must refuse to generate when the clock moves backwards.

   INSIDE                        OUTSIDE
   bookings.property_id -> 7     /properties/89c69134-4b96-49ee-...
   8 bytes, joins fast           16 bytes, unguessable, opaque
   never leaves the process      never used as a foreign key

Follow-up: “Which id do you expose?” Often two per row: a small sequential one internally, an opaque one externally. Sequential ids in URLs leak your volume and invite enumeration. See unique ids.

The case-study openers

Design a URL shortener.

Estimate first: 100M/day is 1,000 writes and 10,000 reads per second, 91 TB over five years. Random seven-character base62 codes with a unique constraint as the guard — the occupancy arithmetic (~5% of the keyspace) is what says seven and says retries terminate. 302 not 301, because analytics and revocability beat the saved traffic. Cache absorbs the reads; partition by month so expiry is a dropped table. See the full walkthrough.

Design a chat system.

The bottleneck is connections, not messages: 10 million concurrent WebSockets at ~10k per server is a thousand machines holding sockets open. That forces a registry of user → connection server plus pub/sub between servers, which nothing else in this track needs. Persist before acknowledging, order by server-assigned ids rather than client clocks, and shard by conversation. See chat.

Design a notification system.

One queue per channel, so a slow SMS provider cannot block email. Preferences and a suppression list keyed by address rather than user. At-least-once with idempotency keys. And rate control as a safety feature — per-user, per-type and global caps that halt and page rather than queue, because the worst bug here sends ten thousand emails with no rollback. See notifications.

Design Airbnb.

0.3 bookings per second at peak, so throughput is not the problem — correctness is. A Postgres exclusion constraint makes overlapping bookings impossible; PENDING is in its status list so checkout holds dates for free. Search from a derived index sunk after commit; listing pages cached (15.2ms to 2.0ms measured). Pricing server-side always. And if you shard, shard by property, because the constraint can only compare rows on the same machine. See Airbnb.

Design Amazon.

Products versus SKUs; attributes as JSON because attribute queries go to the search index, not the database. Facet counts are aggregations in the index, not GROUP BY. Cart lives server-side, holds SKU ids not prices, reserves nothing, and merges on sign-in. Inventory is a conditional update, then relaxed — approximate on read, authoritative at checkout — and real retailers oversell on purpose. Checkout is a saga; authorise before capture. See Amazon.

Design an airline booking system.

Inventory is not a seat count but fare-class buckets, which conveniently distributes contention. Overbooking is a requirement, so the goal is selling exactly the authorised number, not avoiding oversell. Search is a graph problem solved by precomputing route structures offline. Booking is a saga across inventory, payment and ticketing, and ticket issuance must be exactly-once in effect. See airline booking.

Questions to ask them

An interview ends with “any questions?” and the design-relevant ones are worth having ready, because they show you think about systems as things that run rather than things that are drawn.

  • “What is your on-call load like?” — the single most informative question about a system’s real quality.
  • “What is the largest table, and has it been sharded?” — tells you the actual scale, which is usually smaller than the job description implies.
  • “What broke most recently, and what changed afterwards?” — whether incidents produce learning or blame.
  • “How long from merge to production?” — deploy frequency predicts almost everything else about how the team works.

How to say it out loud

Three habits that matter as much as the content, because a correct answer delivered badly scores worse than a decent answer delivered well.

Narrate, including the options you reject. Two minutes of silent thinking looks identical to being stuck. “I could put this in the database or in Redis — Redis, because it is ephemeral and high-write” shows the reasoning that a bare conclusion hides.

Say the cost yourself, before you are asked. Every choice above has one. If everything you propose sounds free, you have either not understood the choices or you are hiding them, and both read the same way from the other side of the table.

Treat a challenge as a hint. “What if that node fails?” is not an attack; it means you have missed something. Think about it rather than explaining why it will not happen — and if you genuinely disagree, say why in terms of the requirements you agreed in step 1.

And know when to say a design is finished. “At 200 requests a second this is one server and a database; here is the arithmetic, and here is the first thing I would change if the traffic were a hundred times larger” is a stronger answer than a sharded multi-region system nobody asked for.

The five sentences worth memorising

If everything above compresses into anything, it is these:

  • Estimate before designing. The numbers tell you which system you are building, and being an order of magnitude out is fine while a thousand times out is not.
  • In transactional systems the difficulty is almost never throughput. All three case studies peak at under 200 writes per second and all three are hard.
  • Prefer making the bad state unrepresentable to coordinating to avoid it. A constraint beats a lock.
  • Say which copy is the truth, and make every other copy rebuildable.
  • Every component needs a stated answer for what happens when it is gone — and if that answer is “degrades gracefully”, test it with the component turned off.

That is the whole track. Start at where to start if you arrived here first.