System Design Interviews: A Repeatable Framework
Stop freezing on "design X." A four-step framework, a worked URL-shortener, and the trade-offs interviewers actually probe.
Datainteg Team
Most candidates do not fail system design interviews because they lack knowledge. They freeze because the prompt feels infinite: "Design a URL shortener" has no obvious first sentence, so the brain stalls, the silence stretches, and panic does the rest. A framework fixes this by replacing the blank page with a sequence — when you always know the next move, you stop performing recall and start doing engineering, which is exactly what the interviewer wants to see.
This article gives you a four-step framework you can run on any prompt, then walks the classic URL-shortener question through every step so you can see the framework actually doing work — including the back-of-the-envelope math, the data model, the architecture above, and the trade-offs interviewers will push on.
Why a framework beats raw knowledge
System design interviews are open-ended on purpose. There is no single correct answer, and the interviewer is not grading whether you memorised the "right" architecture. They are watching how you think under ambiguity: do you ask before you assume, do you reason about scale before you reach for tools, and can you defend a decision when someone pushes back?
A framework helps in three concrete ways. It gives you a guaranteed opening so you never freeze. It makes your thinking legible — the interviewer can follow you and help you when you wander. And it forces you to cover the dimensions that distinguish a senior answer (scale, data, failure, trade-offs) instead of diving into one corner and burning forty minutes there.
The framework below is deliberately boring. Boring is good. You want it so automatic that you can spend your attention on the actual problem, not on remembering what to do next.
The four-step framework
Step 1 — Clarify requirements and scope
Spend the first 5-8 minutes here. The goal is to shrink an infinite problem into a bounded one and to surface the constraints that will drive every later decision.
Separate functional requirements (what the system does) from non-functional requirements (how well it does it: latency, availability, consistency, durability). Then explicitly cut scope. Saying "I'll treat custom aliases and analytics as nice-to-haves and focus on the core shorten-and-redirect path" is a senior move — it shows you can prioritise.
Good questions to ask: Who are the users and how many? What is the read-to-write ratio? What latency is acceptable on the hot path? Do we need strong consistency or is eventual fine? What is the data retention / link lifetime?
Step 2 — Estimate scale with back-of-the-envelope math
Numbers turn vibes into design. You do not need precision; you need an order of magnitude so you can justify caching, sharding, and capacity. State your assumptions out loud, do the arithmetic, and round aggressively. Getting the method right matters far more than the exact figures.
This step is where you decide whether this is a "single Postgres box" problem or a "we need a cache layer and partitioning" problem — and you want that decision to come from numbers, not gut feel.
Step 3 — High-level design, API, and data model
Now draw the boxes. Start with the simplest end-to-end path that satisfies the functional requirements, then name the major components: clients, load balancer, application tier, cache, datastore, and any async pipeline. Define a small, clean API — the request and response shapes — because a concrete contract anchors the rest of the discussion. Sketch the data model: tables or collections, keys, indexes.
Resist the urge to optimise here. A clear, correct, simple design that you can then evolve is stronger than a clever one you cannot explain.
Step 4 — Deep-dive bottlenecks and trade-offs
This is where most of the signal lives. Pick the parts that actually strain under the scale you estimated and go deep: how keys are generated without collisions, how the cache behaves on a miss, how you shard when one database is not enough, what happens when a component dies. For each meaningful decision, state the alternatives and why you chose one — interviewers are listening for "I picked X because Y, and the cost is Z," not for a single confident assertion.
Let the interviewer steer. If they ask "what if writes 10x?" they are inviting a deep-dive; follow them there.
Worked example: design a URL shortener
Let's run the framework end to end.
Step 1 — Requirements
Functional
- Given a long URL, return a short URL.
- Given a short URL, redirect to the original long URL.
- Links should not expire by default (assume a long TTL, e.g. years).
- Optional / out of scope for now: custom aliases, click analytics, user accounts.
Non-functional
- The redirect path must be fast — low latency, since a human is waiting on it.
- High availability: a redirect failing is very visible. We will favour availability over strong consistency.
- Heavily read-dominated: people click links far more than they create them.
- Short codes must be unique and not easily guessable in bulk.
Scoping statement to say out loud: "I'll build the core create-and-redirect path with a cache-backed read, treat analytics as an async add-on, and leave custom aliases as an extension."
Step 2 — Back-of-the-envelope estimation
These numbers are illustrative assumptions for the exercise, not real product data. The point is the method.
Assumptions
- New URLs created: 100 million per month.
- Read-to-write ratio: 100:1 (reads are redirects).
Writes per second
100M / month ÷ (~2.6M seconds/month) ≈ ~40 writes/sec. Call it ~40 QPS of writes, with peaks maybe 5x → ~200/sec.
Reads per second
At 100:1, that is ~4,000 reads/sec average, with peaks of ~20,000/sec. This read-heavy profile is the single most important fact: it justifies a cache and read replicas.
Storage
Per record: short code (~7 bytes) + long URL (~500 bytes) + metadata (timestamps, etc., ~100 bytes) ≈ ~600 bytes, round to ~1 KB to be safe.
100M/month × 1 KB ≈ ~100 GB/month → ~1.2 TB/year. Over 5 years, single-digit terabytes. That fits on a beefy database with room to grow, but it is large enough that a cache for the hot set is clearly worth it.
Key space
Using base62 (a-z, A-Z, 0-9), a 7-character code gives 62^7 ≈ 3.5 trillion combinations — comfortably more than we will ever need at 100M/month for many decades.
The takeaway from the math: this is read-dominated, storage is non-trivial but manageable, and a 7-char base62 code is plenty. That single paragraph justifies most of the architecture.
Step 3 — High-level design, API, and data model
The architecture diagram at the top of this article shows the shape: client → DNS → load balancer → stateless app servers → cache, with the database behind the cache and an async analytics path off to the side.
API
POST /api/v1/shorten
body: { longUrl }
returns: { shortUrl }
GET /{shortCode}
returns: 301/302 redirect to longUrl
A note on the redirect status: a 301 (permanent) lets browsers and proxies cache the mapping, which slashes load on your servers — but it also means you lose per-click visibility and cannot easily change the target. A 302 (temporary) keeps every click flowing through your service, which you want if analytics matter. This tiny choice is a real trade-off interviewers love.
Data model
A single primary table is enough to start:
urls
short_code VARCHAR(7) PRIMARY KEY
long_url TEXT
created_at TIMESTAMP
expires_at TIMESTAMP NULL
The lookup is a primary-key read on short_code, which is as fast as a datastore gets and shards cleanly.
Key generation is the heart of the design. Two main approaches:
- Hash the URL (e.g. take a hash, base62-encode, keep the first 7 chars). Simple, but you must handle collisions by re-hashing, and identical URLs collapse to one code (sometimes desired, sometimes not).
- Counter / key-generation service (KGS). Hand out unique numeric IDs (e.g. from a range-allocating service or a distributed ID generator) and base62-encode them. No collisions by construction, predictable, easy to reason about. The cost is running and scaling the ID service.
I would lead with a KGS that pre-allocates ranges of IDs to each app server, so encoding is a local, lock-free operation and there is no per-write coordination on the hot path.
Step 4 — Deep-dive: bottlenecks and trade-offs
The read path and caching. With ~20k peak reads/sec and a hot set far smaller than the full dataset, a cache (e.g. Redis) in front of the database is the highest-leverage decision. On a redirect: check cache → on hit, return immediately → on miss, read the database, populate the cache, return. Use an LRU policy so the popular links stay resident. Because mappings are effectively immutable once created, cache invalidation — usually the hard part — is mostly a non-issue here.
Sharding. When one database can no longer hold the data or serve the writes, partition by short_code (hash-based sharding). Because every read and write is keyed on short_code, the shard key is obvious and queries stay single-shard — no scatter-gather. This is why choosing the short code as the primary key in Step 3 pays off now.
Availability and failure. Stateless app servers behind a load balancer mean any node can die without data loss; the LB just routes around it. Replicate the database (one primary for writes, replicas for reads) so a primary failure is a failover, not an outage. Since we chose availability over strong consistency, a newly created link being momentarily invisible on a replica is an acceptable trade.
Analytics. Don't write click events synchronously on the hot path. Emit them to a queue and process them asynchronously, so analytics never slows down or breaks a redirect.
Here are the trade-offs an interviewer will probe, and how to reason about each:
| Decision | Option A | Option B | When to pick which |
|---|---|---|---|
| Datastore | SQL (Postgres) | NoSQL (key-value) | Simple PK lookups and huge scale favour a key-value store; pick SQL early for simplicity and if you may add relational features |
| Read strategy | Cache-aside (Redis) | Read from DB replicas only | Cache-aside for the read-heavy hot set; replicas alone are fine at low scale or when the hot set is not concentrated |
| Consistency | Strong | Eventual | Eventual is fine for redirects (a link appearing a second late is harmless); reserve strong for anything money or auth related |
| Key generation | Hashing | Counter / KGS | KGS to guarantee uniqueness and avoid collision handling; hashing when you want identical URLs to dedupe and accept retries |
| Sharding key | By short_code | By creation time | Shard by short_code so every lookup hits one shard; time-based sharding creates hot partitions on recent data |
| Redirect type | 301 permanent | 302 temporary | 301 to offload traffic via client caching; 302 to retain per-click analytics and control |
Signals interviewers look for
- You clarify before you build. You ask about scale, read/write ratio, and consistency instead of assuming.
- You drive the conversation. You move through the steps yourself and don't wait to be prompted at every turn.
- Your numbers inform your design. The cache and sharding decisions visibly come from the estimation, not from reciting a pattern.
- You name trade-offs unprompted. "I'll use eventual consistency here because a redirect appearing a second late is harmless" is gold.
- You start simple, then evolve. A clean baseline you extend under pressure beats a maximal design you can't explain.
- You handle failure. You mention what happens when a node, a cache, or the primary DB dies.
- You manage scope and time. You explicitly defer the nice-to-haves so you finish the core.
Common mistakes
- Jumping straight to boxes. Drawing architecture before clarifying requirements signals you optimise before you understand.
- Skipping the math. "I'll add a cache" with no numbers is a guess; the estimation is what makes it a decision.
- Over-engineering early. Reaching for global multi-region replication and Kafka on a problem that fits one database wastes time and invites questions you can't answer.
- One-way assertions. Saying "I'll use NoSQL" with no alternative or reasoning. Always pair a choice with its cost.
- Going silent. Thinking quietly reads as freezing. Narrate, even when unsure: "I'm weighing hashing versus a counter here."
- Ignoring the read/write asymmetry. Treating a 100:1 read-heavy system like a balanced one and missing the obvious caching win.
- Losing the thread on time. Spending twenty minutes on key generation and never reaching availability or sharding.
Where to practise this
The only way the framework becomes automatic is reps — running it on enough prompts that Step 1 starts before you've consciously decided to begin. Datainteg's interview prep covers exactly this framework, with worked system-design walkthroughs (the URL shortener among them) and practice prompts to rehearse the four steps until they're muscle memory. Use it to drill the motion, not just to read about it.
Key takeaways
- Candidates freeze because the prompt is open-ended; a repeatable framework removes the blank-page problem.
- Run four steps every time: clarify and scope, estimate scale, design high-level (API plus data model), then deep-dive bottlenecks and trade-offs.
- Back-of-the-envelope math is not optional — it's what turns your design choices from guesses into justified decisions.
- For the URL shortener: read-heavy traffic justifies a cache, short_code as the primary key makes sharding trivial, and a key-generation service avoids collisions by construction.
- Every decision should come with its alternative and its cost; one-way assertions read as junior.
- Start simple and evolve under pressure, narrate your thinking, and always cover failure and trade-offs — that's where the senior signal lives.