How to Approach System Design

A repeatable method for designing systems and acing the design interview — clarify requirements, estimate on the back of an envelope, pin down the API and data model, then work high-level to deep-dive to bottleneck, always naming the trade-off.

Most people treat system design as a memory test: enumerate a load balancer, a cache, a message queue, a sharded database, and hope the interviewer nods. That approach falls apart the moment the problem is unfamiliar, because you have no way to decide which components belong and why. The engineers who design well — and who breeze through the interview — are not recalling a parts list. They are running a method: a small set of questions asked in a fixed order that turns a vague prompt into a defensible design.

This post opens a series on that method. It is deliberately component-free. We will not talk about how a specific database replicates or how a cache evicts until later posts. Here the goal is the procedure you apply to every problem, and the one idea that sits underneath all of it: there is no free lunch. Every choice you make buys one property by spending another — latency for consistency, cost for durability, simplicity for scale. Design is the discipline of choosing which bill to pay.


Start by refusing to design

The single most common failure in a design interview is drawing boxes before understanding the problem. A prompt like “design a URL shortener” or “design a news feed” is intentionally underspecified. Your first move is not to reach for a whiteboard — it is to ask questions until the problem has edges.

Split the questions into two buckets.

Functional requirements are what the system does — the observable behaviors. For a URL shortener: create a short link from a long URL, redirect a short link to its target, optionally let users pick a custom alias, optionally show click analytics. Write these as a short list of verbs. If you can’t state them in one breath, you don’t understand the problem yet.

Non-functional requirements are the properties the system must hold while doing those things. These are what actually shape the architecture, and there are six you should ask about every single time:

The gotcha: the interviewer will rarely volunteer these numbers — extracting them is the test. And the answers are not academic. “Read-heavy, eventual consistency is fine, four nines” points you straight at replicas and caching; “write-heavy, strongly consistent, durable” points at a very different, harder design. If you skip this step, every later decision is a guess.


Back-of-the-envelope estimation

Once you know the scale, translate it into numbers before you design, because the numbers decide whether one machine suffices or you need a hundred. This is arithmetic, not engineering, and the interviewer wants to see the arithmetic, not a precise answer.

Two tables make this fast. First, the powers of two — because bytes come in these sizes:

2^10  ≈ 1 thousand   1 KB
2^20  ≈ 1 million     1 MB
2^30  ≈ 1 billion     1 GB
2^40  ≈ 1 trillion    1 TB
2^50  ≈ 1 quadrillion 1 PB

Second, the order-of-magnitude latencies every engineer should carry in their head. These are well-known ballpark figures — not benchmarks from any specific machine — and their ratios are what matter:

L1 cache reference                    ~1 ns
Main memory (RAM) reference           ~100 ns
Read 1 MB sequentially from RAM       ~10 µs
Read 1 MB sequentially from SSD       ~200 µs
Round trip within the same datacenter ~500 µs
Read 1 MB sequentially from SSD       ~1 ms
Disk seek (spinning disk)             ~10 ms
Round trip across continents          ~150 ms

The lesson lives in the gaps: memory is roughly a hundred times faster than SSD, SSD is far faster than a spinning-disk seek, and a cross-continent round trip is glacial next to all of them. This is why caching in RAM beats hitting disk, and why chatty cross-region calls kill latency — you can reason about it from the ratios alone.

Now a worked estimate. Suppose our URL shortener must support 100 million new links per day and is heavily read — say 100 reads per write.

Writes per second (QPS):
  100,000,000 writes/day ÷ 86,400 s/day ≈ 1,160 writes/sec
  (round the day to ~10^5 s to do this in your head: 10^8 / 10^5 = 10^3)

Reads per second:
  1,160 × 100 ≈ 116,000 reads/sec

Peak (assume ~2× average):
  writes ≈ 2,300/sec, reads ≈ 232,000/sec

Storage per link (URL + alias + metadata) ≈ 500 bytes:
  per day:  10^8 × 500 B = 5 × 10^10 B  = 50 GB/day
  per year: 50 GB × 365  ≈ 18 TB/year
  over 5 years          ≈ 90 TB

Read bandwidth (a redirect returns a tiny response, say ~500 B):
  232,000 reads/sec × 500 B ≈ 116 MB/sec outbound

Cache sizing (Pareto: ~20% of links drive ~80% of reads):
  hot set ≈ 20% of one day's links = 2 × 10^7 links × 500 B ≈ 10 GB
  → a hot working set that fits comfortably in the RAM of a single cache node

Nothing here is exact, and that is the point. In under two minutes you learn that reads dominate by two orders of magnitude (so you will lean on caching and read replicas), that five years of data is tens of terabytes (so a single node won’t hold it — you’ll shard), and that the hot set fits in memory (so a cache will absorb most traffic cheaply). The design has already started to take shape, driven entirely by numbers.

The gotcha: never invent precise figures to sound authoritative — “we’ll do 47,000 QPS at 12 ms p99” from thin air is a red flag. Round aggressively (a day is ~10^5 seconds, a year is ~3×10^7), state your assumptions out loud, and let the interviewer correct them. The estimate is a tool for making decisions, not a prediction; being right to one significant figure is exactly enough.


Pin down the API and the data model

With requirements and rough scale in hand, define the contract before the boxes. This forces precision and exposes hidden requirements early.

The API is the set of operations clients can invoke. Keep it small and explicit:

POST /shorten        { long_url, custom_alias? }  -> { short_url }
GET  /{short_code}                                -> 302 redirect to long_url
GET  /{short_code}/stats                          -> { clicks, created_at }

Even this tiny sketch surfaces decisions: is custom_alias allowed, and how do you handle collisions? Does the redirect return 301 (permanent, aggressively cached by browsers — great for load, terrible for analytics) or 302 (temporary, so every click reaches your server and can be counted)? That one status code is a genuine trade-off between offloading traffic and capturing data — and you only notice it by writing the API down.

The data model is the shape of what you store. For our shortener, the core entity is small:

Link
  short_code   string   (primary key)
  long_url     string
  user_id      string   (nullable)
  created_at   timestamp
  click_count  integer

Modeling it raises the next round of questions. How do you generate short_code — a hash of the URL (deterministic, but collisions need handling), or a counter encoded in base-62 (unique by construction, but reveals volume and needs a distributed ID scheme)? Should click_count live on this row (simple, but a hot row under heavy writes) or in a separate append-only analytics store (scales writes, adds complexity)? You do not have to resolve all of these now — but naming them here means your high-level design can address them deliberately instead of by accident.


High-level design, then deep-dive, then bottlenecks

Now, and only now, draw the architecture. Do it in three passes, widening focus each time.

Pass 1 — high-level design. Sketch the major components and the data flow between them, end to end, at low resolution: clients hit an API layer through a load balancer; writes go to a datastore; reads are served from a cache that falls back to the datastore; analytics events flow to a separate pipeline. Boxes and arrows. The goal is a design that plausibly satisfies the functional requirements — no more. Resist the urge to detail any single box; you are proving the whole thing hangs together.

Pass 2 — deep-dive. Pick the components where the non-functional requirements actually bite and zoom in. Given our estimate — read-heavy, tens of terabytes, a small hot set — the interesting boxes are the cache (what do we cache, how do we invalidate, what happens on a miss) and the datastore (how do we shard 90 TB, what’s the partition key, how do reads find the right shard). Spend your time where the numbers said the pressure is, not on the parts that are obviously fine.

Pass 3 — bottlenecks and trade-offs. Now attack your own design. Where does it fall over? If the cache node dies, does a thundering herd of 232,000 reads/sec stampede the database? If one shard holds a viral link, is it a hotspot? What’s the single point of failure? For each weakness, name the fix and its cost: adding cache replicas improves availability but risks serving stale data; adding a write-ahead queue smooths write spikes but adds latency and a component to operate. Stating the cost is what separates a designer from a box-drawer.

The gotcha: the flow is a spiral, not a straight line. Discovering a bottleneck in pass 3 often sends you back to revise the data model or even re-ask a requirement (“do we actually need real-time click counts, or is a one-minute delay fine?”). Interviewers reward that loop — it shows you’re reasoning, not reciting. What they penalize is diving into the deepest detail of one component before the high-level design exists.


The one idea underneath all of it: no free lunch

Every decision in this series will come back to a single principle: you cannot maximize everything at once. The famous constraints make this concrete. The CAP theorem (Gilbert and Lynch’s formalization of Brewer’s conjecture) says that when the network partitions — and it will — a distributed system must choose between remaining consistent and remaining available; it cannot have both during the partition. That is not a law about databases specifically; it is a law about the universe the system lives in.

The same tension appears everywhere, wearing different clothes:

The one idea underneath all of it: no free lunch
You want more… You usually pay with… Example
Lower latency Freshness / consistency Serve from cache, risk staleness
Stronger consistency Higher latency / lower availability Synchronous cross-region writes
Higher availability More cost / more staleness Extra replicas across zones
Higher durability Higher write latency Wait for N replicas to acknowledge
More scale More complexity Sharding, coordination, operations
Less complexity Less scale / less flexibility One big database, until it isn’t enough

Good design is not the absence of trade-offs — it is choosing the right trade-off for the requirements you extracted in step one. That is why the method starts with requirements and ends with trade-offs: the first step tells you which properties you’re allowed to spend, and the last step is where you spend them on purpose.


What the rest of this series covers

This opener is the method. The posts that follow apply it to each major dimension of system design, and every one of them is a study in the trade-off above:

  1. How to approach system design (this post) — the method and the core tension.
  2. Scaling — vertical vs. horizontal, load balancing, statelessness, and why “just add servers” hides real coordination costs.
  3. Caching — where to cache, invalidation strategies, and the trade of latency for freshness.
  4. Databases and storage — SQL vs. NoSQL, indexing, partitioning, and sharding a dataset that outgrows one machine.
  5. Consistency and consensus — replication, quorums, and the algorithms (Paxos, Raft) that let machines agree despite failures.
  6. Asynchronous processing — queues, event streams, and decoupling producers from consumers to absorb spikes.
  7. Reliability and resilience — redundancy, failover, timeouts, retries, and designing for the failure that will happen.
  8. A full worked design — one problem, end to end, running the entire method from clarifying questions to bottleneck analysis.

Each post assumes the method from this one. When you read “we’ll cache the hot set,” the questions in your head should already be: what’s the read-to-write ratio, what does an in-memory read save us over a disk read, and what does stale data cost the user? If those questions come automatically, the method has done its job.


Key takeaways


Further reading