Home › High-Level Design › How to Approach System Design
H

How to Approach System Design

You can learn to drive a system design round in an hour. Not master it. Drive it. The difference between a pass and a fail is almost never knowledge of some exotic database. It is whether you ran the room like someone who has shipped things. I have sat on the other side of the table for hundreds of these, and the candidates who pass sound calm, ask the right questions early, and say their trade-offs out loud. Here is how they do it.

High-Level DesignMedium11 min read

If you remember five things

The whole method in one glance. Everything below is how to execute these five moves under pressure.

  1. It is a conversation, not a monument. You are graded on how you think when the answer is not obvious, not on the final diagram.
  2. Name the category first. Read-heavy, write-heavy, low-latency, or strong-consistency. That one call decides which trade-offs eat your time.
  3. Run the same five phases every time. Clarify, estimate, API plus model, draw, then scale. Memorize the budget so you never freeze.
  4. One number drives the design. Surface it in estimation (reads per second, say) and justify every later choice against it.
  5. Start dumb, then break it on purpose. Draw the simplest thing that works, then find the weakest component and fix it out loud, naming what you give up.

1The mental model (what this really is)#

A system design round is not a quiz about Kafka. It is a 45-minute simulation of you joining a design review and being handed a vague problem.

  • The interviewer plays a product manager who gives you one sentence ("design a URL shortener") and watches what you do with it.
  • There is no single correct design for "design Twitter." There are good trade-offs and bad ones.
  • The job is to show whether you can tell which is which.

The trap most people fall into.

  • They think the goal is the final diagram. It is not.
  • The goal is to show how you think when the answer is not obvious.
  • A clean diagram built in silence scores worse than a messy one you reasoned through out loud.

So the loop you are running is simple.

  • Reduce ambiguity into a concrete set of requirements.
  • Turn those into a rough back-of-envelope scale.
  • Sketch a design that meets that scale.
  • Deliberately break your own design and fix it. Out loud. The whole time.

A system design answer is a conversation about trade-offs, not a monument you build in silence.

The one thing being measured

I am not scoring whether you memorized a reference architecture. I am scoring whether, handed a vague one-liner, you can turn it into requirements, numbers, a design, and a defended trade-off. The diagram is evidence of that process, not the product.

2How to recognize what kind of problem you have#

Before you touch a whiteboard, figure out which flavor of problem you were handed. Four show up constantly.

  • Read-heavy at scale. News feed, timeline, product catalog. Reads outnumber writes 100:1 or worse. The whole game is caching and fan-out.
  • Write-heavy or ingestion. Metrics pipelines, logging, analytics events, "design a system to collect clicks." The game is buffering, batching, and not losing data.
  • Low-latency or real-time. Chat, presence, live location, ride matching. Websockets, in-memory state, geo-partitioning.
  • Strong-consistency or money. Payments, ticket booking, inventory, "no double-charge." Transactions, locks, idempotency keys.

The signal is in the verb and the noun.

  • "Design a system that counts video views" screams write-heavy plus eventual consistency.
  • "Design seat booking for a stadium" screams contention plus strong consistency.
  • Name the category in your head in the first two minutes. It tells you which trade-offs you will spend your time on.
From the interviewer's chair

When a candidate says "this is a read-heavy problem, so I expect to spend my time on caching and fan-out" in the first two minutes, I relax. They just told me they have a map. When someone jumps straight to boxes without naming the shape of the load, I brace for a generic answer that fits no problem in particular.

3The template (how it works)#

Use the same five-phase structure every time. Memorize it so you never freeze. Rough budget for a 45-minute round:

PHASE 1 Requirements & scope ~6 min
PHASE 2 Estimates (back-of-envelope) ~5 min
PHASE 3 API + data model ~5 min
PHASE 4 High-level design (draw it) ~12 min
PHASE 5 Scale, bottlenecks, deep dive ~15 min
 (leave 2 min of slack)

A pseudocode version of your own behavior:

def system_design_round(prompt):
 functional, non_functional = clarify(prompt) # ask, don't assume
 scope = pick_top_3(functional) # say what's OUT of scope
 qps, storage = estimate(non_functional) # numbers, out loud
 api = define_endpoints(scope)
 model = data_model(api)
 draw(client -> lb -> service -> db) # simplest thing that works
 while interviewer_pushes_harder():
 bottleneck = find_weakest_component()
 fix = pick_tradeoff(bottleneck) # name what you give up
 draw(fix)
 summarize(what_you_built, what_you_cut)

Here is that same flow as a diagram, so you can picture the arc of the round:

flowchart TD A[Clarify requirements] --> B[Estimate scale] B --> C[Define API and data model] C --> D[Draw simplest design] D --> E{Interviewer pushes} E -->|yes| F[Find weakest component] F --> G[Pick a trade-off and redraw] G --> E E -->|no time left| H[Summarize built and cut]
The arc of the whole round. Notice the loop: once the simple design is up, the interviewer pushing and you redrawing is the main event, not a detour. Phases 1 through 4 exist to get you into that loop fast with time to spare.

Two rules that matter more than they look.

  • State what is out of scope in Phase 1. Saying "I will not handle analytics or spam detection today, I will focus on posting and reading the feed" tells me you can prioritize. Juniors try to build everything and run out of time.
  • Start Phase 4 with the dumbest design that could possibly work, then let the interviewer's questions pull you toward complexity. Lead with a six-queue microservice mesh and you will never finish, and it reads as pattern-matching instead of reasoning.

4Flagship walkthrough: design a URL shortener end to end#

I am going to run this one the way I would want to see it in a real loop, including the dialogue. This is the model to copy. Every other problem is this skeleton with a different hard part.

Phase 1: clarify (the dialogue)#

Do not monologue. Trade questions. Here is roughly how the first three minutes should sound.

Candidate: Before I design anything, let me pin down scope. Roughly how many new links do we create per day?

Interviewer: Call it 100 million a day.

Candidate: And is this read-heavy? I'd expect redirects to dominate link creation.

Interviewer: Yes, reads dominate. Assume reads are about 100 times writes.

Candidate: Do links expire, or live forever?

Interviewer: Five-year retention is fine.

Candidate: Two more. Do we need custom aliases, like a vanity short code someone picks themselves? And do we need click analytics?

Interviewer: Custom aliases are optional. Basic click counts are a nice-to-have, not core.

What those answers lock in.

  • I commit out loud: "I will focus on two things, create a short link and redirect at scale."
  • "I will treat analytics and spam detection as out of scope for now, and I will mention where custom aliases change the design."
  • That one sentence, naming what is in and what is out, is worth real points. It is the single most common thing juniors skip.

Phase 2: estimates (real numbers, and the one that drives the design)#

This is where you win cheap points. Do the arithmetic out loud.

  • Writes: 100M new links per day divided by 86,400 seconds is about 1,200 writes per second.
  • Reads: reads are 100x writes, so about 120,000 reads per second.
  • Storage: each record is maybe 500 bytes (short code, long URL, timestamps, metadata). 100M per day times 365 days times 5 years is about 1.8e11 records. Times 500 bytes is roughly 90 TB over five years.

Three numbers fall out, but only one drives the design.

  • 1.2K writes/sec, 120K reads/sec, 90 TB.
  • The one that matters is 120,000 reads per second. Say so.
  • That number is why the redirect path gets a cache and a CDN, and why I shard for read-key locality.
  • The write side at 1.2K/sec is almost boring by comparison; one sharded database swallows it.
  • When an interviewer asks "what is the hardest part here," the honest answer is: serving 120K redirects per second cheaply and consistently. Everything downstream is in service of that number.
~120K
Reads per second, the number that drives everythingIt is why the redirect path gets a cache plus CDN and why I shard by code for clean single-key reads. Anchor every later choice to this one figure and the whole design reads as deliberate.

Phase 3: API and data model#

Keep the API tiny. Two endpoints cover the core scope.

POST /shorten {long_url, custom_alias?} -> 201 {short_url}
GET  /{code}                            -> 302 redirect to long_url

The data model is one table. Here it is as a real schema:

Column Type Notes
code VARCHAR(7) Primary key, base62, 7 chars, the shard key
long_url TEXT The destination we redirect to
created_at TIMESTAMP When the link was made
expires_at TIMESTAMP Null means no expiry; drives the TTL sweep
creator_id BIGINT Null for anonymous; used later for quotas

Why 7 characters?

  • Base62 (a-z, A-Z, 0-9) with 7 chars gives 62^7, about 3.5 trillion combinations.
  • We need about 180 billion over five years, so 7 chars leaves comfortable headroom.
  • That little calculation shows you reason from the numbers, not from memory.
  • If pushed, I would note that even 6 chars (62^6, about 57 billion) is cutting it close against 180 billion, which is exactly why I went to 7.
Pause before you read on: you need two options for generating the short code. One lets you dedup identical URLs for free but adds a read before every write. The other is collision-free but needs coordination. At 1,200 writes/sec, which do you pick and why?
Reveal the answer
Pick the counter (collision-free, coordinate once per block of IDs). At 1.2K writes/sec, coordinating once per 1,000 IDs is trivial, while a read-before-write on every single insert taxes the hot write path for a dedup benefit nobody asked for. The right instinct is to let the write rate pick the trade-off, not habit.

How do we generate the code? Two honest options.

  • (a) Hash the long URL and take 7 chars, handle collisions with a retry.
  • (b) A global counter or ID generator, then base62-encode the integer.

I would pick (b) with a pre-allocated key range per server, so we do not hit the counter on every write.

  • Trade-off to name out loud: hashing gives free dedup (same URL yields the same code) but adds a read-before-write on every insert.
  • The counter approach is collision-free but needs coordination.
  • At 1.2K writes/sec I would rather coordinate once per block of IDs than read-before-write on every single insert. I go counter.

Phase 4: high-level design (draw the dumb version first)#

Start simple, then add exactly one thing the numbers demand.

  • Client, load balancer, stateless app servers, database. That is the dumb version.
  • The 120K reads/sec demands a cache in front of the redirect path mapping code -> long_url.
  • A short code maps to one long URL forever, so these entries are extremely cacheable.
  • A cache hit returns a 302 in about a millisecond.
flowchart LR U[Client] --> LB[Load balancer] LB --> APP[Stateless app servers] APP --> CACHE[(Redis cache)] APP --> DB[(Sharded links DB)] IDG[ID generator] -. hands ID blocks .-> APP CDN[CDN edge] --> LB
The simplest design that meets the numbers. Only one box here is not boilerplate: the Redis cache on the redirect path. Everything else is client, load balancer, stateless servers, and a database. Add complexity only when a question forces it.

The two paths through this picture.

  • Write (POST /shorten): client to load balancer to an app server, which grabs the next ID from its local pre-allocated block, base62-encodes it, writes the row, and warms the cache.
  • Read (GET /{code}): check Redis first; on a hit return the 302 immediately, on a miss read the shard, backfill the cache, and redirect.

Phase 5: scale, bottlenecks, and the deep dives#

Now pressure-test it against the numbers.

  • 90 TB does not fit on one box, so I shard the links table by the short code (hash of code maps to a shard).
  • Reads are a single-key lookup, so sharding by code is clean: no cross-shard queries.
  • The cache absorbs the 120K reads/sec, so the database mostly sees writes plus cache misses.
  • I add a CDN or edge layer for the hottest links.
  • For writes at 1.2K/sec, a single sharded database handles it easily; the ID generator is the one piece needing care, so I hand out ID ranges in blocks of 1,000 to each app server to avoid a hot coordinator.

Notice what I did not reach for.

  • No fancy anything. A cache, a shard key, and one careful look at the ID generator.
  • The numbers told me those were the only places that hurt.
  • Now go deep on the three parts an interviewer will actually probe.

Deep dive A: the ID generator (the signature hard part)

This is the piece that can quietly fail a candidate, so I treat it as the headline.

  • The naive design is a single global counter that every write increments.
  • At 1,200 writes/sec that counter is a single point of contention and a single point of failure.
  • The fix is block allocation: each app server claims a range (say IDs 1,000,000 to 1,000,999) in one atomic step, then serves those 1,000 IDs from local memory with zero coordination.
  • When it exhausts the block, it claims the next one.
  • The coordinator now sees roughly 1 request per 1,000 writes, so about 1.2 requests/sec instead of 1,200.
sequenceDiagram participant App as App server participant Alloc as ID allocator App->>Alloc: claim next block Alloc-->>App: range 1000000 to 1000999 Note over App: serve 1000 IDs from memory App->>Alloc: claim next block Alloc-->>App: range 1001000 to 1001999
Block allocation in one picture. The allocator is touched once per 1,000 IDs, not once per write, which is how a 1,200/sec hot spot becomes a 1.2/sec cold one. The app server serving IDs from memory between claims is the whole trick.

The trade-off to name out loud.

  • If an app server crashes mid-block, we lose the unused IDs in that block. That is completely fine.
  • The ID space is 3.5 trillion wide, so burning a few hundred IDs on a crash costs nothing, and in exchange we removed the hot coordinator.
  • For the allocator itself I would use a small highly available store (a single row with a compare-and-set on the high-water mark), or ZooKeeper-style coordination if we want multiple allocators.
  • Saying "losing IDs on crash is acceptable because the keyspace is 3.5 trillion" is exactly the kind of quantified trade-off that reads as senior.

Deep dive B: the hot key (one viral link)

Uniform sharding assumes traffic spreads evenly across codes. It does not.

  • One link goes viral and 50% of the 120K reads/sec hits a single code, so one shard is on fire while the rest idle.
  • First line of defense is the cache plus CDN I already have: an immutable code -> long_url entry is trivially cacheable at the edge, so the hot link gets served from CDN POPs and never touches the shard.
  • If the hot key is still hammering a single Redis node, I replicate that key across cache replicas and read from any of them, since the value never changes.
  • The point to make: because the mapping is immutable, hot keys are a caching problem, not a consistency problem, which is the easy kind to solve.

Deep dive C: cache strategy and consistency

Because a short code maps to one long URL forever, the cache can never go stale in the normal sense, which makes this close to a free lunch.

  • I use a simple read-through cache with a long TTL and let the CDN in front handle the hottest keys.
  • The one subtlety is cache misses on cold links: an old link nobody has clicked in months is not in Redis, so the first click pays one shard read, then it is warm again. That is fine.
  • The only real invalidation events are expiry (below) or an admin deleting a malicious link, and for those I explicitly delete the cache entry rather than waiting for TTL.
  • Call that out, because "how do you handle deletes in an otherwise-immutable cache" is a question I would ask.

5Variations and gotchas#

The same skeleton stretches to most prompts, but watch the hinge points.

  • "Add analytics or click counts." Now a write-heavy sub-problem is glued to a read-heavy one. Do not increment a counter on the hot redirect path synchronously. Fire an event to a queue, aggregate offline. Say "I do not want analytics writes to slow down redirects."
  • Custom aliases. These collide with generated codes. Keep a reserved namespace or check a uniqueness index on insert. This turns a collision-free design back into read-before-write for that path. Call it out.
  • Expiry. 5-year retention plus a TTL means a background job sweeping expired rows, or a TTL index. Mention the cleanup job; forgetting it is a classic miss.
  • Hot keys. One link goes viral: 50% of traffic hits one code. Your nice uniform sharding is now useless because one shard is on fire. The fix is the cache plus CDN, which you already have. Good that you can point to it.

6Trade-offs (the part interviewers actually grade)#

Every real design is a choice between things you cannot have at once. Keep these pairs on the tip of your tongue and say which side you are taking and why.

  • Consistency vs availability. CAP is the textbook version, but the practical question is: during a network partition, do you reject writes or accept them and reconcile later? For a feed, accept and reconcile (eventual). For a bank balance, reject (strong). Pick, then justify.
  • Latency vs freshness. A cache makes reads fast but can serve stale data. For a URL shortener, stale is basically impossible (immutable mapping), so caching is free lunch. For a stock ticker, a 2-second-stale cache is a bug.
  • Normalization vs read speed. Joins are clean but slow at scale. Denormalize (store the author's name inside each post) and you buy fast reads at the cost of update fan-out.
  • Push vs pull (fan-out). On write (push) makes reads cheap but writes expensive, bad for celebrities with 100M followers. On read (pull) is the reverse. Real systems go hybrid: push for normal users, pull for the handful of mega-accounts. Knowing that hybrid exists is a senior signal.

Then say the number that drives the choice.

  • "Reads are 100x writes, so I will pay on the write side and cache aggressively" is the sentence that makes an interviewer nod.
  • Every pair above has a number behind it. Name the number, then name your side.

7Mistakes that cost people the round#

  • Jumping to a diagram before asking anything. Instant red flag. You just told me you will build the wrong thing fast at work too.
  • No numbers. If you never estimate QPS or storage, every later choice is a guess. Even rough numbers beat none.
  • Buzzword soup. Naming Kafka, Cassandra, and Kubernetes without saying why reads as memorized, not understood. I will ask "why Cassandra and not Postgres here?" and the soup dries up.
  • Building silently. If you go quiet for three minutes drawing boxes, I have nothing to grade. Narrate.
  • Over-engineering. Six services for a problem that needs two. Complexity is a cost, and good engineers spend it carefully.
  • Ignoring the interviewer's nudges. When I say "what happens if that queue backs up?", that is a gift. It is the exact area I want to probe. Chase it.
  • Running out of time in Phase 1. Spending 20 minutes clarifying scope and never drawing anything. Watch the clock.
The red flag that ends rounds fastest

Drawing boxes before you have asked a single question. The moment you start designing a login flow or a six-service mesh off one vague sentence, I have learned the thing I most need to know: you will build the wrong thing confidently. Ask, scope, and name what is out, before anything touches the whiteboard.

8Your practice path#

Do these in order. Say every phase out loud, ideally to another person or a recording. Reading solutions is not practice; talking is.

  1. URL shortener (warm-up, read-heavy plus ID generation).
  2. Pastebin or image host (adds blob storage, CDN, TTL).
  3. Rate limiter (token bucket, where to store counters, single-node vs distributed).
  4. Twitter or news feed (the fan-out push-vs-pull question, the classic).
  5. WhatsApp or chat (websockets, online presence, message ordering, delivery receipts).
  6. Uber or nearby drivers (geo-sharding, quadtrees, real-time matching).
  7. Ticketmaster or seat booking (strong consistency, locking, no double-booking).
  8. YouTube or Netflix (video upload pipeline, transcoding, CDN at huge scale).

By problem 4 you will notice the template is identical and only the hard part moves. That recognition is the skill.

9Where this shows up#

Every senior and most mid-level software roles at big tech include at least one system design round.

  • Google, Meta, Amazon, Microsoft, Netflix, Uber, Stripe, and effectively every funded startup hiring experienced engineers.
  • At staff level it is often two rounds and weighted heaviest of all.
  • Infra, backend, and platform teams lean on it hardest; even some frontend and mobile roles now run a scaled-down version.
  • The framework above is the same one strong candidates use whether the prompt is a URL shortener or a global payments ledger.
  • Learn the five phases cold, and the only thing left to practice is the hard middle of each problem.
Follow-ups the interviewer will ask (and your answers)
"Why did you pick a counter over hashing for the short code?"
Hashing gives free dedup but adds a read-before-write on every insert to handle collisions. The counter is collision-free and only needs coordination once per block of IDs. At 1,200 writes/sec, coordinating once per 1,000 beats taxing every write, so I take the counter. The trade-off I accept: no automatic dedup of identical URLs, which the spec did not ask for.
"Your global counter is a single point of failure. Fix it."
Block allocation. Each app server atomically claims a range of 1,000 IDs and serves them from memory, so the coordinator sees about 1.2 requests/sec instead of 1,200. If a server crashes mid-block we lose a few hundred IDs, which is nothing against a 3.5 trillion keyspace. The allocator itself is a small highly available store with a compare-and-set on the high-water mark.
"One link goes viral and 50% of reads hit one code. What happens?"
That one shard is on fire while the rest idle. Because the mapping is immutable, this is a caching problem, not a consistency one. The CDN edge serves the hot link from POPs so it never touches the shard, and if a single Redis node still gets hammered I replicate that key across replicas and read from any of them. The value never changes, so replication is safe and free.
"They ask you to add click analytics. Does that change the design?"
Yes, it bolts a write-heavy sub-problem onto a read-heavy one. I do not increment a counter synchronously on the redirect path, since that would slow the hot read. I fire an event to a queue and aggregate offline, so analytics writes never touch redirect latency. That keeps the 120K reads/sec path exactly as fast as before.
"You have five minutes left and barely started. What do you cut?"
I protect the arc over the detail. I state the chosen trade-offs in one sentence each (cache plus CDN for reads, shard by code, block allocation for IDs), name what is out of scope, and skip the deep dive I have not reached. A complete thin answer beats a half-built deep one, and saying "here is what I would do next with more time" shows I know where the remaining risk lives.

What you can now explain in the room

  • Why a system design round grades your reasoning out loud, not the final diagram.
  • How to classify a prompt into read-heavy, write-heavy, real-time, or strong-consistency, and why that call decides where your time goes.
  • The five-phase template and the rough minute budget for each phase of a 45-minute round.
  • How to derive the numbers for a URL shortener and why 120K reads/sec is the one that drives the design.
  • Why block allocation turns a hot ID counter into a cold one, and why losing IDs on a crash is fine.
  • The core trade-off pairs (consistency vs availability, latency vs freshness, push vs pull) and how to name the number behind your choice.

Want to see which version of this round each company runs and how they grade it? Check the company guides to match this framework to the room you'll actually sit in.

Get a prep plan built around your How to Approach System Design interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your How to Approach System Design prep plan is on the way.