Blog · 2026-01-24 · 21 min read
System Design Interview Cheat Sheet
The forty-five minute system design interview written out as a framework - the clarifying questions, the estimation arithmetic, a worked URL shortener, data modelling, caching, consistency and the reliability patterns interviewers expect.
A system design round is not a trivia test about which component you have heard of. It is a structured conversation in which the interviewer is trying to find out how you reason when the requirements are incomplete and the answer will not fit on a whiteboard. Almost nobody arrives with the right design memorised. What separates a strong candidate from a weak one is whether the reasoning is visible, whether the numbers are roughly right, and whether the tradeoffs are named out loud.
The thing most candidates lack is not knowledge. It is a written-out shape to walk through, so that when the pressure is on they improvise badly. This post is that shape, plus the arithmetic and the vocabulary to fill it in.
The forty-five minute shape
Split the time deliberately. Interviewers do not expect you to guess this split correctly on your own, but they reward you for staying inside it, so state it at the start: "I would like to spend the first few minutes on requirements, then a rough estimate, a high-level design, a deep dive on the part you find most interesting, and we will leave a few minutes at the end for questions."
- Clarify, about five minutes. Nail down what the system must do and what "good" means for it.
- Estimate, about five minutes. Derive the scale so the design is sized rather than guessed.
- High-level design, about fifteen minutes. Sketch the components and the flow through them.
- Deep dive, about fifteen minutes. Pick one or two components and go down properly. This is where the score is.
- Wrap, about five minutes. Bottlenecks, what breaks first, and the interviewer's questions.
Spending a long time in the clarification phase is normal and correct, not a failure. A twenty-five minute clarification that ends with "so we are optimising reads, the creator can update at any time, and we keep links for two years, which means a CDN in front and a key-value store on the read path" has saved you from designing the wrong system for the remaining twenty minutes. Rushing past it is the single most expensive mistake in the room, because every later decision inherits whatever you failed to pin down.
If the interviewer cuts you off and says "skip the requirements, just design it", accept that immediately and move on. Do not defend the process.
The clarification questions to ask
Ask these explicitly. Asking them one at a time from memory under pressure is where candidates forget three of them.
Functional requirements. What can a user actually do? What is the core action and what are the surrounding ones? For a URL shortener: create a short link, resolve a short link, optionally customise the alias, optionally see click analytics, optionally expire a link. What is explicitly not in scope? Naming the non-goals is one of the strongest moves available to you, because it shows you can hold a boundary.
Non-functional requirements. Scale first, then the rest:
- Scale. How many users, how active, how many requests each? Read-heavy or write-heavy, and what is the read-to-write ratio?
- Latency. What is an acceptable response time, and at what percentile? A p99 of 200 ms is a completely different design from a p99 of two seconds.
- Availability. What is the target, and is there an explicit number attached to it? What happens to the product if this is unavailable for an hour?
- Consistency. Where must a read reflect a write immediately, and where is a few seconds of staleness acceptable? This single question separates a payments design from a feed design.
- Durability. How much data loss is acceptable? Once written, is it required to survive forever, or for a retention window?
Read/write ratio. Get a number. A ten-to-one ratio changes your topology completely: reads can be served from replicas or a cache while writes go to a single primary.
Retention. How long does data live, and what deletes it? Retention is what makes storage estimation and compaction questions come up later.
Data sensitivity. Is any of it personal, financial or regulated? Is there an audit requirement, an encryption requirement, a data residency requirement? If the interviewer says "payments", the answer to almost every follow-up is "consistency, and never lose a write".
Multi-region. Is this global from day one, or one region? Are users in multiple geographies, and does latency to them matter?
Budget and team. This surprises people and it is fair to ask. "What order of magnitude are we designing for, and how much operational complexity does the team want to own?" It licenses you to propose managed services over self-run infrastructure, and it shows you are not designing a system nobody will operate.
Back-of-the-envelope estimation
You do not need precise numbers. You need numbers that are within an order of magnitude and that lead somewhere. Everything below is arithmetic you can do in your head.
Powers of two. One kilobyte is 10^3 bytes, one megabyte 10^6, one gigabyte 10^9, one terabyte 10^12. Once you stop converting exactly and start approximating by powers of two, the arithmetic gets fast.
The constants worth memorising.
| Quantity | Value |
|---|---|
| Seconds in a day | 8.6 x 10^4 |
| Seconds in a year | 3.15 x 10^7 |
| One million requests per second | 10^6 |
| Peak-to-average ratio | commonly 2x to 5x |
| 1 KB x 10^6 | about 1 GB |
| 1 KB x 10^9 | about 1 TB |
Conversions you will actually do. Average QPS is daily requests divided by 8.6 x 10^4. Peak is average times your skew factor. Storage per year is bytes per entity times entities per year. Bandwidth is bytes per response times requests per second, and it is the number that most often justifies a CDN, because bandwidth is billed and latency is felt.
For the worked example below: a hundred million short links created per year, each row roughly 200 bytes, gives about 2 x 10^10 bytes per year, so around 20 GB a year of raw row data. That is small. What is not small is resolution traffic: a hundred billion resolutions a year at roughly 400 bytes each is about 4 x 10^13 bytes a year, tens of terabytes of egress. The write path is not the interesting part of this system, and the estimate is what tells you so. This is the single most valuable thing estimation does: it redirects the design.
Little's law. Concurrency equals throughput multiplied by latency. One thousand requests per second at 100 ms average means about a hundred requests in flight at any moment. Use it to size connection pools and worker counts, and say it out loud because interviewers recognise it.
A worked example: a URL shortener
Carried all the way through, because seeing the phases run in order is worth more than the individual techniques.
Requirements
Assume the interviewer says: "design a system where users can shorten URLs, and anyone can resolve them." Clarify and land on: create and resolve are the core actions; users can optionally pick a custom alias; links optionally expire; click counts are nice to have but not on the critical path; we want roughly a hundred million links a year and reads hugely dominate writes; sub-100 ms resolution in most regions; availability matters more than consistency, because the worst outcome of a stale read is a stale click count, not a wrong link. State that explicitly: a read that returns a link that was deleted one second ago is acceptable; a read that returns the wrong link is not.
Estimation
A hundred million creations a year is around 300 per second average, so peak is perhaps 1500 writes per second. That is small. If the product grows to a thousand links per user and there are a hundred million users, that is a hundred billion stored links, so about 20 TB of row data and, at 400 bytes per resolution, tens of terabytes of egress per year. Conclusion out loud: writes are not the problem, reads and bandwidth are, so this is a read-path design.
API
POST /links { target_url, custom_alias?, ttl_days? } -> { code, short_url }
GET /links/{code} -> { code, target_url, created_at, expires_at }
GET /links/{code}/stats -> { clicks_total, clicks_7d, clicks_30d }
DELETE /links/{code} -> 204
Note what is missing: no synchronous click tracking on the resolve path. That is a deliberate decision, and saying it is the point.
Data model
CREATE TABLE links (
code CHAR(7) PRIMARY KEY,
owner_id BIGINT NOT NULL,
target_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ
);
CREATE INDEX links_owner_created_idx ON links (owner_id, created_at DESC);
The primary key is the lookup key. That decision drives everything downstream, so justify it: seven characters is short enough that the whole key fits in one cache entry and one index page, so a point lookup is a single seek rather than a filter.
High-level sketch
client -> DNS/CDN (caches by /{code}) -> edge service -> [Redis: code -> target]
|
+-> KV store (links) on miss, with TTL
+-> analytics pipeline -> columnar store (click counts)
writer -> API service -> Kafka topic: links.created -> KV store writer (single consumer, ordered)
Two things to point at while you draw. The read path is a cache-aside with a TTL, so the origin is hit only on a miss. And clicks do not go through the API service at all; they go into a log stream and are aggregated later, so the hot path has no write at all.
Deep dive: the click count that will break you
This is the interesting problem and it is the one to choose, because it has a real failure mode. The naive version increments a counter on the database row during resolution. At peak read volume that is a write per read, on the hot path, to the same row for popular links. A popular link becomes a row-level hot spot, and eventually a single row becomes the whole system's problem.
The answer has three layers:
- Do not write synchronously. Emit an event per resolution to a log or stream. Buffer in the service, flush in batches, and let the aggregator do the counting. The read path now has no synchronous write at all.
- Shard the aggregation by time, not by link. Roll up per-minute counters keyed by
codeinto a store that is cheap to write to, then roll those up into hour and day buckets in a columnar store. Click queries read pre-aggregated data, which is exactly the case for a columnar layout. - Make the consumer idempotent. The stream will redeliver. Give each event a unique id, enforce uniqueness at the sink, and treat a duplicate as success rather than as an error.
Then say what you give up: click counts become eventually consistent, seconds behind rather than immediate, and the stats endpoint has a bounded staleness. For this product that is obviously the right trade, and having said so out loud is worth more than the design itself.
Data modelling
Choosing a primary key. It should be the key your dominant access pattern uses, it should be small, and it should be roughly uniform. Surrogate auto-increment ids are convenient and wrong for anything sharded, because they concentrate writes and require a lookup to get the real key. Hash-derived ids are fine when the access pattern is a point lookup and you do not need range scans. Natural keys are fine when they are stable and truly unique. Say which one you picked and what it optimises.
Indexing strategy. Index the columns your queries actually filter and join on, in the order they appear in the query, and keep the index narrow. Know the difference between a B-tree index, which is right for range scans and equality, and a hash index, which is right for equality only and gets you nothing on ordering. Be ready to name the write cost: every index is maintained on every insert and makes writes slower. An index nobody uses is pure overhead, so find out how the dropped plans are monitored.
Choosing a store.
- Relational, when the data is genuinely relational, you need transactions across rows, and the query shape is not fully known yet. Joins, constraints and a query planner are features.
- Document, when an entity is read and written whole, the schema varies by client, and you want to fetch one object in one round trip. You give up cross-document joins and some constraint enforcement, so be ready to say what enforces the invariant in your design instead.
- Wide-column, when you are doing large scans and aggregations over time-series-shaped data, and you will read many columns for a few rows rather than many rows for a few columns.
- Key-value, when you have one dominant access pattern of point lookups, as in this shortener.
The honest framing is that the choice follows the access pattern, and the access pattern follows the requirement you clarified in the first five minutes. "MongoDB because it is schemaless" is not a data modelling decision.
When denormalisation is justified. It is justified when your read path needs a join on every request, when the read-to-write ratio is high, and when you can tolerate the data being temporarily out of sync. Pre-join into a wide row or a document, write asynchronously, keep the source of truth intact, and make the denormalised copy rebuildable. The cost you must name out loud is write amplification plus a repair path, because a denormalised copy that cannot be rebuilt is just a second source of truth that lies.
Scaling vocabulary
Say these correctly and it signals you have actually operated systems.
Vertical scaling means a bigger machine. It is simple, it is almost always the right first move, and it has a hard ceiling plus a downtime problem when you need the bigger machine. Horizontal scaling means more machines, which needs everything below.
Stateless services are the precondition for horizontal scaling. Session state moves out of the process and into a shared store, or into a signed token. If you cannot answer "where does the session live", you cannot scale out.
Load balancers distribute across instances. Know that a load balancer is also a single point of failure unless it is itself replicated, and that health checks which only check the process are not enough if the process is up but its database connection is dead.
CDN moves static and cacheable content to the edge. It is the cheapest latency and bandwidth win available, and it is the first thing to reach for when reads dominate and content is cacheable.
Read replicas scale reads. They add replication lag, which means read-your-own-writes stops working unless you pin the writer, so this is a consistency decision as much as a scaling one.
Sharding splits data across nodes. Three strategies:
- Hash sharding on a key. Spreads load evenly and makes range scans impossible.
- Range sharding on a key like a timestamp or an id. Preserves ordering and locality, and produces hot spots, because the newest range takes all the writes.
- Directory or lookup-table sharding where a service maps key to shard. Maximum flexibility, and you have just built a distributed lookup service that must itself be highly available. Often the right answer for a multi-tenant system where a single large tenant would otherwise dominate one shard.
Consistent hashing is how you place keys on nodes such that adding or removing a node moves only about 1/n of the keys. You will be asked why it is better than modulo hashing: modulo remaps nearly everything when n changes by one, which means a cold cache and a thundering herd on the database. Mention virtual nodes, because real nodes have uneven load and one physical node per shard does not fix that.
Replication versus partitioning is the distinction people get wrong, and getting it wrong is visible. Replication copies the same data to other nodes so you have more readers or a failover target. Partitioning splits the dataset so each node holds a different subset and total capacity scales. Replication does not increase your write capacity or your storage ceiling; partitioning does. Most large systems do both: partition the data, and replicate each partition.
Caching
What to cache. Cache the reads that dominate and that tolerate staleness. Rank layers by value: browser cache, then CDN, then reverse proxy, then in-process memory, then a shared key-value cache, then the database's own query cache. Higher up the stack means cheaper, so move down only when you must.
TTL. Every entry needs one. A cache with no TTL is a correctness bug waiting for a deploy to trigger it. Pick the TTL from how stale the product can tolerate, not from what is convenient, and be able to say what the user sees at the end of the TTL window.
Invalidation strategies.
- Cache-aside, also called lazy loading. The app reads the cache, and on a miss reads the database, populates the cache and returns. The default, because the application stays in control and the cache never becomes a source of truth.
- Write-through updates the cache on every write. Never serves stale data, and every write now depends on the cache being up.
- Write-behind, also called write-back, writes to the database and updates the cache asynchronously. Fast writes, and the cache can permanently diverge if the async update fails, so you need reconciliation.
The stampede problem. When a popular key expires, every concurrent request misses at once and they all hit the database together. That is how a cache turns into an outage. The fixes, and you should name at least one: a short lock or mutex around the fill so one request does the work and the rest wait or serve stale; a probabilistic early expiry where a small fraction of requests refresh before the TTL ends, which spreads the cost out; serving stale content while one request refreshes in the background; and request coalescing so concurrent misses for the same key share one upstream call.
Consistency
What CAP actually says. Under a network partition, a distributed system must choose between returning a response that might be stale and refusing to respond. That is the theorem. What CAP does not say is that you get to pick at design time and forget about it. Partitions are not an option you avoid; they are a permanent condition of any multi-node system, which means the choice is made continuously, per operation, and often differently for different operations in the same system. The popular simplification, that you must choose between consistency and availability and that one system has picked one, is why people get surprised: a single system routinely refuses to respond for one operation and serves a stale read for another.
Consistency means linearizability: every operation appears to take effect instantaneously at some point between its invocation and its response, and all replicas agree on the order. Partition tolerance you essentially always need. Availability is the part you decide.
Eventual consistency means replicas converge if writes stop. It is not a weaker guarantee than a race condition, it is a specific guarantee with a convergence condition, and the engineering work is making the window small and bounded and observable.
Read-your-writes is the guarantee users actually notice. The cheap way to get it is a sticky session or reading from the primary right after a write. The general way is a version or sequence number: the write returns a token, the read requires at least that token, and the system waits or serves from the leader until it is satisfied. Also be ready to name the sibling guarantees you will be asked about: monotonic reads, read-your-writes, write-follows-reads, and causal consistency.
The patterns for writes that span services:
- Two-phase commit. The coordinator prepares every participant, then commits. It is the only pattern here that gives real atomicity across systems, and it is blocking, has coordinator failure modes, and holds locks across the network. It is correct for low-volume, latency-tolerant work, and it is a bad default for a high-throughput request path.
- Saga. Break the transaction into a sequence of local transactions, each with a compensating action. No locks, no blocking coordinator, and you must write the compensations and accept that an intermediate state is externally visible.
- Outbox pattern. When the real problem is "the database write and the message publish must not diverge", the answer is not a distributed transaction at all. Write the business row and an outbox row in one local transaction, then have a relay publish the outbox row and mark it published. At-least-once delivery, so the consumer must be idempotent, and the invariant holds. This is the right answer far more often than two-phase commit.
Say the words "at-least-once delivery plus idempotency" rather than "exactly once". There is no exactly-once across systems; there is at-least-once plus deduplication.
Reliability
Every one of these is a pattern, and every one of them is a fair deep dive.
Timeouts are the first thing to add and the most commonly missed. A call with no timeout holds a thread and a connection until something else gives up, which is how one slow dependency becomes a whole-service outage. Set an explicit deadline on every outbound call and propagate a shorter deadline than the one you were given.
Retries need exponential backoff and jitter. Fixed intervals from many clients that failed together produce a thundering herd that re-creates the outage. And every retry needs a bound, because unbounded retries convert a dependency's degradation into your own resource exhaustion.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function withRetry(fn, { attempts = 4, baseMs = 50, maxMs = 2000, isRetryable } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const isLast = attempt === attempts - 1;
if (isLast) throw err;
if (isRetryable && !isRetryable(err)) throw err;
const ceiling = Math.min(maxMs, baseMs * 2 ** attempt);
await sleep(Math.random() * ceiling);
}
}
}
await withRetry(() => fetch(url, { signal: AbortSignal.timeout(500) }), {
attempts: 4,
isRetryable: (err) => err.name === 'TimeoutError' || err.code === 'ECONNRESET',
});
Math.random() * ceiling is full jitter: randomise across the whole interval instead of sleeping a fixed amount, so clients that failed together do not retry together. Also retry only idempotent operations, and catch narrowly, because a blanket retry of every error will happily retry your validation failures.
Circuit breakers stop calling a dependency that is failing. After a threshold of failures within a window, fail fast for a cool-off period, then let a single probe through. The point is to fail fast rather than hold your own resources waiting for a dependency that is not coming back. Half-open state, where a limited number of probes are allowed, is what stops the breaker flapping.
Bulkheads isolate resource pools so one failing dependency cannot consume the threads, connections or memory everyone else needs. Separate thread pools per downstream call, or separate concurrency limits per dependency, so a slow service degrades one feature instead of the process.
Dead-letter queues are where messages that cannot be processed go. Without one, a poison message is retried forever and blocks or endlessly re-processes. Dead-letter, alert, and have a human-or-script path for draining it.
Graceful degradation is a product decision, not an infrastructure one: what still works when a dependency is down, and can you shed the rest. A write path that cannot reach its analytics sink should drop or buffer events, not fail the user's request.
Questions to ask
The interviewer will offer time for these, and skipping them reads as disengagement. Ask about the thing that determines whether you would be good at the job:
- What is the hardest problem the team owns right now, and what happens to whoever owns it?
- How does the team decide what to build, which tells you whether your opinion is wanted?
- What was the failure mode in the last incident, and what changed afterwards?
- What does on-call look like here, and how often does it page?
- How is correctness defined for this system, and where is the line between "we can be stale here" and "we cannot"?
- Where is the read/write ratio, and has it moved in the last year?
What interviewers are actually scoring
Almost none of it is whether your design matches theirs. A competent candidate and an interviewer will produce different architectures for the same prompt and both can be right. What is being graded:
- Did you clarify before you designed? This is checked in the first five minutes and it colours everything.
- Did you derive numbers, or assert them? Asking where a figure comes from matters more than the figure.
- Can you name the tradeoff you chose and what you give up? Every design closes something. A candidate who presents only upside has not thought about it.
- Do you know where the hard part is? Systems have one. Finding it and choosing to spend your deep dive there is a senior signal.
- How do you reason about failure? Timeouts, retries, partial failure, what the user sees when a dependency is down.
- Can you be talked out of your design? Disagreement with evidence is a good sign. So is changing your mind when the evidence changes.
Common mistakes
Jumping to technology before requirements. "I'd use Kafka and Cassandra" as an opening move is the clearest possible signal that requirements were skipped. Ask what the data is and what it is used for first.
Over-engineering. Six technologies in forty-five minutes, none of which the numbers justify. The interviewer is usually wondering whether you would build something this complex for a hundred requests a second. Resist the shiny component unless you can say what it buys.
Ignoring the data model. Skipping straight to a diagram. The data model is where the access pattern becomes visible and it is frequently what the deep dive is actually about.
No numbers at all. "It's going to be large" is not an estimate. Even a rough order of magnitude gives the interviewer something to work with, and you can correct yourself when challenged.
Fighting the clock on one component. Spending thirty minutes on the read path and then having nothing for failure. If you notice you are overrunning, say so and move: "I want to spend the last few minutes on what breaks first, can we jump there?"
Choosing a component to show off. If the interviewer is visibly uninterested in Kafka, do not build the answer around Kafka. Watch where they lean in and spend the deep dive there.
Related reading: the HR round post covers the non-technical side of the same process, the five-day preparation plan covers how to get to the interview ready, and the Amazon backend post covers how a design round gets probed when behavioural evidence is weighted as heavily as the architecture.
Want the full set?
- 360 questions, every one with a model answer
- Scalability & reliability cheat sheet
- Back-of-envelope formulas
- The 6-step answer framework
System Design Cheat Sheet Pack
The vocabulary and numbers behind every system design answer, plus a repeatable structure for the 45 minutes.
Get this pack Free Frontend pack