Worked Example: Designing a URL Shortener
You'll learn to
- -Apply the full 5-step design loop to a complete, worked system
- -See how ID generation, caching, and a deliberate consistency choice combine in one real design
Every chapter so far has taught one piece of the toolbox in isolation. This is the first of three chapters that instead start from a bare prompt (the way an actual interview does) and walk the entire 5-step loop end to end, out loud, the way you would in the room. First up: the classic warm-up question, "design a URL shortener" (like bit.ly or TinyURL).
Step 1: Clarify Requirements
- -Functional: a user submits a long URL and gets back a short one; visiting the short URL redirects to the original. Optional: custom aliases, link expiration, click analytics.
- -Out of scope (say this out loud): user accounts, link editing after creation, spam/abuse detection; naming what you are NOT solving is itself a signal.
- -Non-functional: redirects must be fast (sub-100ms) and highly available; a shortener that is slow or down breaks every link that uses it, everywhere. Creation can tolerate slightly more latency than redirects.
Step 2: Estimate Scale
Assume 100 million new short links created per month, and, critically, a read-heavy skew, since a link is written once but clicked many times. A realistic read:write ratio for this kind of system is around 100:1.
Notice what this math already told you before drawing a single box: this system is overwhelmingly read-dominant and the total data volume is small enough to fit on a single well-chosen database: the hard part is not storage, it is serving 20,000 reads/sec with low latency.
Step 3: High-Level Design
The app server asks a dedicated ID generator for a unique value, encodes it, and writes the mapping.
A cache absorbs the vast majority of the 20,000 reads/sec; the database only sees a cache miss.
Step 4: Deep Dive - Generating the Short Code
This is the one piece an interviewer usually wants you to go deep on. Two real approaches:
- -Hash + collision check: MD5/SHA the long URL, take the first 7 characters, check if that code is already taken, retry on collision. Simple, but that collision check is an extra read on every single write, and collisions get more frequent as the table fills up.
- -Counter + base62 encoding: maintain a globally unique, monotonically increasing counter and base62-encode it (0-9, a-z, A-Z, giving 62^7 ≈ 3.5 trillion possible 7-character codes). No collisions, ever, by construction. The catch: a single counter is a single point of contention at 40 writes/sec, which is exactly the problem the Unique IDs module already solved with pre-allocated ID ranges per server, so no request ever waits on a shared counter.
This is a direct callback to Module 6: the same "give each server a pre-claimed range of IDs" technique you learned there is precisely what removes the counter as a bottleneck here.
Step 5: Trade-offs & Failure Modes
- -301 (permanent) vs. 302 (temporary) redirect: a 301 gets cached by the user's browser, so repeat clicks never even hit your servers (great for load, but you lose the ability to track that click or change the destination later). A 302 costs you a server hit every time but keeps you in control. Most link shorteners choose 302 specifically to keep click analytics.
- -Consistency: checking "does this code already exist" during creation needs strong consistency (two users must never be handed the same code), but propagating a newly created link to caches/replicas can be eventually consistent; a few seconds of delay before a brand-new link resolves everywhere is an acceptable trade, not a correctness bug.
- -Failure mode (cache goes down): every read now falls straight through to the database. At 20,000 reads/sec peak, that could overwhelm it. The fix is a rate-limited fallback (serve a fraction of traffic, degrade gracefully) rather than letting every request pile onto a suddenly-uncached database, the same cascading-failure risk the Circuit Breakers chapter covers.
Why not just use an auto-incrementing database ID as the short code directly?
"That would work fine and it's simple."
"It works, but it leaks information: competitors could estimate your daily link volume by watching the counter increase, and sequential codes are trivially guessable/enumerable. Base62-encoding a counter from a range you've been pre-allocated (not the raw row ID) keeps codes short and collision-free without exposing the literal creation order or total count."
Why is a URL shortener described as "read-heavy"?
Level 3: URL Shortener in the HLD Lab is exactly this system - go build the design you just walked through.