System design — the framework that doesn't crumble
📖 Walk me through it — plain English
A system design question asks you to sketch how a real product works under the hood — "Design a URL shortener," "Design a chat app." There is no single right answer and no code to compile. The interviewer is watching how you think: do you ask good questions, make sensible trade-offs, and stay organized when the problem is huge and vague? The whole point of this lesson is that you don't need to be a senior architect. You need one framework — a fixed set of steps — that you walk through every single time so you never freeze.
The framework here is six steps, in order. Let me define the jargon as it shows up. Step 1, Clarify: pin down what to build before building. Functional requirements are features ("users can post a tweet"); non-functional requirements are qualities ("it must load in under a second, even for millions of users"). DAU = Daily Active Users (how many people use it per day). QPS = Queries Per Second (how many requests hit your servers each second). The read/write ratio tells you whether people mostly view data or mostly create it — a news feed is read-heavy, a logging system is write-heavy — and that single fact steers most later decisions.
Step 2, API: list 3–5 endpoints — the named operations a client can call, like createPost(userId, text) — with their inputs, outputs, and who calls them. Step 3, High-level diagram: draw the boxes and arrows. The default flow is Client → Load Balancer (a traffic cop that spreads requests across many servers so none gets overwhelmed) → app servers → Cache (a small, fast memory store like Redis that holds hot data so you don't hit the slow database every time) → Database. Step 4, Data model: decide what tables/entities you store, which indexes (lookup shortcuts that make searches fast) you need, and whether to use SQL (rigid tables with strict relationships) or NoSQL (flexible, easier to scale across many machines) — and crucially, why. Step 5, Deep dive: the interviewer points at one box and says "explain this." You defend that choice in detail. Step 6, Scale: they say "now 10× the traffic" and you reach for the standard tools — more caching, sharding (splitting one giant database into smaller pieces across machines), async queues (a buffer like Kafka that lets slow work happen in the background instead of making the user wait), and a CDN (servers near users that cache images/files so they load fast worldwide).
An everyday analogy: it's like designing a new restaurant. You don't start by buying ovens. First you clarify ("fast food or fine dining? how many guests a night?"). Then you write the menu (the API — the fixed list of things people can order). You sketch the floor plan — door, host stand, kitchen, pantry (the diagram). You decide how the pantry is organized so cooks find ingredients instantly (the data model and indexes). The interviewer then quizzes you on one station — "walk me through the kitchen" (deep dive). Finally they say "a tour bus of 200 just pulled up" and you adapt: prep food ahead, add a second register, keep popular dishes warm up front (scale: queues, more servers, caching). Same playbook every time, whatever the restaurant.
How to actually use it in the room:
- Say the steps out loud and follow them in order. Narrating "First let me clarify requirements…" signals structure and buys thinking time.
- Do the napkin math in Step 1. Roughly: DAU × actions-per-user ÷ 86,400 seconds ≈ QPS. Numbers justify every later choice (e.g. "10k writes/sec is why I'd add a queue").
- Always justify with "because." "NoSQL because the data is simple key-value and we need easy horizontal scaling" beats naming a technology with no reason.
- Start simple, scale only when asked. Draw the basic Client→LB→app→cache→DB picture first; add sharding/CDN/queues in Step 6 when the interviewer turns up the load.
- Know the building blocks cold: load balancer, cache, SQL vs NoSQL, message queue, CDN, blob storage, search. They're the Lego bricks every answer is built from.
Why this works: there's too much to invent on the spot, so a memorized skeleton means you're never staring at a blank wall. Going general-to-specific (requirements → boxes → one deep component → scaling) mirrors how real systems are actually built, and it keeps you and the interviewer on the same page. You're not graded on knowing every technology — you're graded on a clear, justified, organized thought process, and the framework hands you exactly that.
Even early-career interviews now include design questions. You don't need staff-level depth — you need a framework you can always fall back on.
The on-ramp: what a "system" even is
Before the framework, the picture. When you open an app on your phone, your phone is the client — the program in front of the user. It does not hold the real data; it sends a request over the internet to a server — a computer running in a data center whose job is to answer those requests. The server reads and writes a database — durable storage that survives restarts (your tweets are still there tomorrow because the database wrote them to disk). A "system" is just these pieces wired together so they handle far more users than one machine ever could. System design is the craft of choosing those pieces and the wires between them.
A handful of words come up in every interview. Learn them once and the rest of the lesson reads easily:
- Functional requirement — a feature the system must do ("users can shorten a URL and click it later"). Non-functional requirement — a quality it must have ("clicks resolve in under 100 ms; the service is up 99.9% of the time"). Functional = the what; non-functional = the how well.
- Latency — how long one request takes, end to end (e.g. 50 ms). Lower is better; it's the wait the user feels. Throughput — how many requests the system finishes per unit time (e.g. 10,000 per second). Higher is better; it's total capacity. A checkout lane: latency is how long your order takes, throughput is how many shoppers clear per minute. They trade off — a packed lane (high throughput) often means a longer personal wait (high latency).
- QPS / RPS — Queries (or Requests) Per Second. The two terms are used interchangeably: the number of requests arriving each second. The single most useful number for sizing a system.
- Availability — the fraction of time the system is up and answering, written as "nines." 99.9% ("three nines") allows ~8.7 hours of downtime a year; 99.99% ("four nines") allows ~52 minutes. SLA (Service-Level Agreement) — the promised number you commit to, often with penalties if you miss it. Saying "I'll target a 99.9% availability SLA" tells the interviewer you know reliability has a measurable bar.
- Vertical scaling — make one machine bigger (more CPU/RAM). Simple, but there's a ceiling and a single point of failure. Horizontal scaling — add more machines and split the work across them. Harder to coordinate, but scales nearly without limit. "Scale up" = vertical; "scale out" = horizontal. Most large systems scale out.
- Load balancer — a component that sits in front of many identical servers and spreads incoming requests across them so no single server is overwhelmed (and so one dead server doesn't take the site down). The traffic cop that makes horizontal scaling actually work.
- Cache — a small, very fast store (usually in memory, e.g. Redis) that keeps copies of frequently-requested data so you can answer without touching the slow database. A cache hit is found-in-cache (fast); a cache miss means you fall through to the database and then store the result for next time.
- CDN (Content Delivery Network) — a fleet of cache servers spread around the world. A user in Tokyo gets images/videos/files from a nearby CDN node instead of your one server in Virginia, cutting latency dramatically. Best for static content that rarely changes.
- Sharding / partitioning — splitting one giant dataset into pieces ("shards") that live on different machines, so each machine holds only a slice. Keys A–M on shard 1, N–Z on shard 2. This is how a database scales out past one machine's disk and write capacity. (Partitioning is the general idea; sharding is partitioning across separate machines.)
- Replication — keeping copies of the same data on multiple machines. A primary takes writes and copies them to replicas; replicas serve reads (so you can handle far more reads) and provide a backup if the primary dies. Sharding splits data to spread load; replication copies data for read scale and durability. Systems often do both.
- SQL — relational databases (Postgres, MySQL) with strict tables, rows, columns, and built-in support for joins (combining related tables) and transactions (all-or-nothing updates). Great when relationships and consistency matter. NoSQL — non-relational stores (key-value like DynamoDB/Redis, document like MongoDB, wide-column like Cassandra) with flexible schemas that shard across machines easily. Great for simple access patterns and massive scale, at the cost of joins and some consistency guarantees.
- API (Application Programming Interface) — the contract between client and server: the exact set of operations a client can call, with their inputs and outputs. In a web system these are usually HTTP endpoints (e.g.
POST /urls). The API is the menu; everything behind it is the kitchen. - Bottleneck — the one part that runs out of capacity first and caps the whole system, like the narrowest point of a funnel. The art of scaling is finding the current bottleneck, relieving it, then finding the next one.
- Clarify. Functional vs non-functional. Read/write ratio. DAU. QPS. Storage growth.
- API. 3–5 endpoints. Inputs, outputs, callers.
- High-level diagram. Client → LB → app → cache → DB. Draw it.
- Data model. Entities, indexes, SQL vs NoSQL — and why.
- Deep dive. Interviewer picks one component. Defend choices.
- Scale. 10× growth. Caching, sharding, async queues, CDN.
The six steps, walked end-to-end: design a URL shortener
Abstract steps only stick once you watch them solve a real problem, so here is the canonical warm-up question — a URL shortener (think TinyURL or bit.ly): take a long link and hand back a tiny one like short.ly/aZ8kP that redirects to the original when clicked. We'll run all six steps in order, exactly as you would aloud in the room.
Step 1 — Clarify (and do the napkin math)
Resist the urge to draw. First nail down scope by asking and stating the requirements yourself:
- Functional: (1) shorten a long URL into a short code; (2) redirect a short code to its original URL. Maybe: custom aliases, expiry dates, click analytics — confirm whether these are in scope or out.
- Non-functional: redirects must be fast (low latency, <100 ms) because every click waits on them; high availability (a dead shortener breaks every link ever made — target 99.9%); short codes never collide or get reused.
- Read/write ratio: people click links far more than they create them. Assume ~100 reads (redirects) per 1 write (new short URL). This is the most important sentence in the whole answer — it tells us the system is heavily read-dominated, which screams "cache aggressively."
Now the back-of-the-envelope estimate. Assume 100 million new URLs created per month. Turn that into per-second rates and storage:
# WRITES (new short URLs)
# 100,000,000 per month / ~2,600,000 seconds in a month
writes_per_sec = 100M / 2.6M # ~= 40 writes/sec (small!)
# READS (redirects) — 100x reads-to-writes
reads_per_sec = 40 * 100 # ~= 4,000 reads/sec
# STORAGE over 5 years
total_urls = 100M * 12 * 5 # 6,000,000,000 rows (~6 billion)
bytes_per_row = ~500 bytes # short code + long URL + metadata
total_storage = 6B * 500 # ~= 3 TB (fits on a few disks, not exotic)
What the math buys you: 40 writes/sec is trivial — a single database handles that easily, so we do not need write-sharding on day one. 4,000 reads/sec is bigger and latency-sensitive, which confirms the read-heavy strategy: a cache will absorb most of it. 3 TB over five years is real but unremarkable. Saying these numbers out loud, even rough, is exactly what separates a strong candidate from a hand-waver.
Step 2 — API
Define the contract: a tiny set of endpoints with inputs, outputs, and who calls them. Keep it to the essentials.
# Create a short URL (called by: the user / client app)
POST /urls
body: { longUrl: "https://example.com/very/long/path" }
returns: { shortUrl: "https://short.ly/aZ8kP" }
# Resolve a short code to its original (called by: the browser on click)
GET /{shortCode} # e.g. GET /aZ8kP
returns: 301 redirect -> longUrl # 301 = "moved permanently"
Two endpoints cover the whole product. The redirect is a standard HTTP 301 (permanent redirect) so the browser jumps straight to the long URL. Mention that you'd add auth/rate-limiting on POST later — that shows awareness without bloating the core.
Step 3 — High-level diagram
Draw the boxes and arrows. Start with the default flow and place the cache deliberately, because Step 1 told us reads dominate. In a text whiteboard:
┌──────────────┐
Client ────▶ │ Load Balancer│ (spreads requests across app servers)
(browser) └──────┬───────┘
│
┌──────────┴──────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ App server│ ... │ App server│ (stateless; add more to scale out)
└─────┬─────┘ └─────┬─────┘
│ 1) check cache │
▼ │
┌───────────┐ │ cache HIT -> return longUrl fast
│ Cache │◀─────────────┘
│ (Redis) │
└─────┬─────┘
│ 2) cache MISS -> read DB, then fill cache
▼
┌───────────┐
│ Database │ (shortCode -> longUrl)
└───────────┘
Read path for a click: Client → Load Balancer → App server → look in Cache. On a hit, return the long URL immediately (microseconds). On a miss, read the Database, store the result in the cache, then return it — so the next click for that code is a hit. Because short URLs are read over and over and almost never change, the cache hit rate is very high, which is exactly why this design comfortably serves 4,000 reads/sec without a giant database. The app servers hold no per-user state, so the load balancer can send any request to any of them and we scale by adding more boxes.
Step 4 — Data model (and the key trick)
We store one kind of thing: a mapping from short code to long URL.
# Table / collection: urls
short_code # e.g. "aZ8kP" <-- PRIMARY KEY, indexed for instant lookup
long_url # the original URL
created_at # timestamp
created_by # optional: which user (for analytics/limits)
SQL or NoSQL? Justify it. Our access pattern is "given a short code, fetch its long URL" — a pure point lookup by key, with no joins and no complex queries. That is the textbook case for a NoSQL key-value store (DynamoDB, or even Redis-backed): it shards trivially across machines and does single-key reads blazingly fast. A relational SQL database would also work fine at this scale and gives easy secondary indexes if analytics grow, so either is defensible — the reason is what scores. The short_code is the index/primary key, so a lookup is O(1)-ish rather than scanning 6 billion rows.
How do we make short codes? Two clean approaches, and naming the trade-off impresses: (1) Hash the long URL and take the first few characters — simple, but two URLs can collide, so you must detect and retry. (2) Counter + Base62: keep a global auto-incrementing number (1, 2, 3, …) and encode it in Base62 (the 62 characters 0–9 a–z A–Z). Base62 packs a big number into few characters — 7 Base62 characters give 62⁷ ≈ 3.5 trillion codes, far more than our 6 billion. This guarantees uniqueness with no collisions, at the cost of needing a way to hand out counter values across many servers (a known sub-problem you can flag).
Step 5 — Deep dive (interviewer picks a box)
Now the interviewer points at one component: "Tell me more about the cache." This is where you defend a choice in detail. A strong answer for our shortener:
- What to cache: the hot
short_code → long_urlmappings. The most-clicked links dominate traffic (a few viral links get millions of hits), so even a modest cache captures most reads. - Strategy: cache-aside (lazy loading) — on a miss, the app reads the DB and writes the value into the cache. Set a TTL (time-to-live) so entries expire and the cache doesn't grow forever.
- Eviction: when the cache is full, drop the least recently used (LRU) entries — they're the least likely to be clicked again.
- Consistency: short URLs are effectively immutable (a code always points to the same long URL), so stale-cache problems barely exist here — a major reason caching is so safe and effective for this design.
Notice the pattern: pick the component, then talk through what, how, failure modes, and why it fits this problem. The same shape works if they instead probe the database, the code-generation scheme, or the load balancer.
Step 6 — Scale (now 10× the traffic)
"Suppose we get 40,000 reads/sec and 400 writes/sec." Reach for the standard tools, in order of cheapest-first, each aimed at the next bottleneck:
- More caching + read replicas. Reads are the pressure. A bigger/clustered cache absorbs the bulk; add database replicas so misses spread across several read copies instead of one primary.
- CDN / geo-distribution. Put cache nodes near users so a click in Tokyo doesn't cross an ocean. Cuts latency and offloads your core.
- Shard the database. 6B+ rows outgrow one machine. Partition by short code (e.g. by a hash of the code) so each shard holds a slice — this scales storage and write capacity horizontally.
- Async queue for side work. Click analytics, logging, and counting don't need to block the redirect. Push those events onto a message queue (Kafka/SQS) and process them in the background, keeping the user-facing path fast.
- Scale app servers horizontally. They're stateless, so just add more behind the load balancer.
Crucially, you started simple in Steps 3–4 and only added this machinery when the interviewer raised the load. That ordering — minimal viable design first, scale on demand — is itself a thing they're scoring.
The same six steps, on a paste bin. Swap the example and the skeleton doesn't move. Clarify: store a blob of text, return a link, optional expiry — write-once, read-many, so still read-heavy. API: POST /pastes → returns an id; GET /{id} → returns the text. Diagram: same Client→LB→app→cache→DB, but the big text bodies go in blob storage (S3) while the database holds just id → blob location. Data model: id (key), blob URL, expiry, created_at; key-value lookup again. Deep dive: how expiry purges old pastes (a TTL/background sweeper). Scale: CDN the popular pastes, shard by id, async-delete expired ones. Different product, identical playbook — which is the entire point of having a framework.
What interviewers actually score
There's no answer key, so it helps to know what the rubric really rewards. Across companies it's some version of:
- Structure. You drive a clear process (the six steps) instead of wandering.
- Requirements first. You scope before you build, and you do the napkin math.
- Justified trade-offs. Every choice has a "because" tied to the requirements/numbers.
- Identifying bottlenecks. You can say what breaks first at 10× and fix it deliberately.
- Communication. You think aloud, draw, and check in ("does this scope sound right?").
- Jumping to architecture before requirements. Drawing boxes in minute one is the #1 mistake — every later choice is unfounded.
- Naming tech with no reason. "I'll use Kafka and Cassandra" with no why reads as buzzword bingo.
- Skipping the estimate. Without QPS/storage numbers you can't justify caches, shards, or queues.
- Over-engineering early. Sharding a 40-write/sec system on day one signals you can't right-size.
- Going silent. A correct design thought silently scores zero — narrate.
- Load balancer (L4 vs L7)
- Cache (Redis) — read/write-through, write-back
- SQL — when, indexes, replicas
- NoSQL — KV vs document vs wide-column
- Message queue (Kafka / SQS) — async, decoupling
- CDN, blob storage (S3), search (Elasticsearch)
- URL shortener (next lesson)
- Newsfeed (read- vs write-heavy fan-out)
- Rate limiter (token bucket vs sliding window)
- Chat (push vs pull, presence)
- Distributed file storage
- Web crawler (politeness, dedup)
A quick gloss on the building-blocks list so it isn't cryptic: L4 vs L7 load balancers route by raw network address (layer 4, fast/dumb) versus by request content like the URL path (layer 7, smarter). Write-through / write-back are cache strategies — write-through updates cache and DB together (safe), write-back updates the cache first and the DB later (faster, riskier). KV / document / wide-column are NoSQL flavors: key-value (a giant dictionary), document (JSON-like records), wide-column (rows with flexible huge sets of columns, e.g. Cassandra). Blob storage (S3) holds big files — images, videos, the paste-bin text bodies. Search (Elasticsearch) indexes text so you can query "find all posts containing X" — something plain databases do poorly.
Takeaway: a system design answer is a process, not a memorized diagram. Walk the six steps in order — Clarify (functional vs non-functional, read/write ratio, QPS and storage math) → API → diagram (Client→LB→app→cache→DB) → data model (entities, indexes, SQL vs NoSQL, with a reason) → deep dive (what/how/failures/why) → scale (caching, replicas, sharding, async queues, CDN, applied to the real bottleneck). Start simple, attach a "because" to every choice, do the napkin math, and narrate. The framework is the answer.