Design bit.ly — end to end
This is a system design question — the interviewer hands you a vague product ("build a URL shortener") and watches how you turn it into a concrete, scalable design out loud. There is no trick algorithm to discover; the skill being graded is whether you can structure a whole service in an organized way: nail down what it must do, design the interface, choose how to store things, trace what happens on each request, and show the design survives a flood of traffic. Below is the full plain-English on-ramp, followed by the six-step worked answer interviewers expect, with every term defined the first time it appears.
📖 Walk me through it — plain English
A URL shortener like bit.ly does two simple things: you hand it a long ugly link, it gives you a tiny one like bit.ly/aB3xK9z; later, when someone clicks the tiny one, it sends them to the original. That second action is called a redirect — the server tells the browser "the thing you want actually lives over here, go there instead." This lesson is a system design question: there's no clever algorithm to find, the skill being tested is whether you can structure a whole service — its interface, storage, and how it survives lots of traffic — out loud, in an organized way. The lesson walks the standard 6-step framework interviewers expect.
The everyday analogy: think of a coat-check at a theater. You give the attendant your bulky coat (the long URL) and they hand you a small numbered ticket (the short code — the few characters after the slash, e.g. aB3xK9z). The ticket is short on purpose — easy to pocket, easy to read back. When you return and show the ticket, they walk to the exact hook with that number and return your coat. The number itself is meaningless; it's just a compact key (a unique label you look something up by) that points to one stored item. A URL shortener is a giant, automated coat-check for web links.
A few words you'll see throughout, defined once in plain English so nothing below is a mystery:
- Short code: the tiny string after the domain (
aB3xK9z) that uniquely identifies one long URL. - base62 encoding: writing a number using 62 symbols — the digits
0–9, lowercasea–z, and uppercaseA–Z(10 + 26 + 26 = 62). Because each character carries more "value" than a plain digit, big numbers pack into very few characters: 7 base62 chars hold up to 62⁷ ≈ 3.5 trillion distinct values. - Counter / ID-generation: a source of guaranteed-unique numbers that only ever goes up (1, 2, 3, …). Encode each in base62 and every short code is automatically unique — no two requests can ever get the same number.
- Hash: a function that scrambles an input (the long URL) into a fixed-size fingerprint; you keep the first few characters as the code. Fast, but two different URLs can scramble to the same fingerprint — a collision.
- Collision: two different inputs landing on the same short code. The redirect would then be ambiguous, so any design that can collide needs extra logic to detect and resolve it.
- Read vs write: a write is creating a new short link; a read is someone clicking one (a redirect lookup). The read/write ratio tells you which to optimize — here it's roughly 100 reads per write, so reads dominate.
- Cache: a small, very fast store (usually in memory) that holds recently/frequently used answers so you don't re-hit the slow database. Redis is the usual choice.
- CDN: a Content Delivery Network — a fleet of servers spread around the world ("edge" locations) that sit physically near users, so responses travel a short distance and arrive fast.
- 301 vs 302 redirect: two flavors of "go elsewhere."
301= "moved permanently" — the browser remembers it and skips your server next time (cheap, but you can't count those clicks).302= "found / temporary" — the browser asks your server every time (lets you count clicks, more load). - Key-value (KV) store: a database built for one job — "give me the single value stored under this exact key" — with no multi-table joins. Perfect when every lookup is "short code → long URL."
- Sharding: splitting one giant dataset across many machines (shards), each holding a slice, so no single box has to store or serve it all.
- Capacity estimate (back-of-envelope): rough math — links/year, requests/second, gigabytes of storage — done in your head to size the system and justify each choice. Interviewers love seeing the numbers, not just the boxes.
Here's how the 6 steps in the lesson fit together, in order:
Why this design holds up: the whole thing works because reads dominate and the cache is cheap and fast — keeping the hot links in memory means the slow database is barely touched. Base62 over a counter guarantees every code is unique without ever scanning for collisions (each counter value appears exactly once), unlike hashing the long URL, where two different inputs could collide and you'd need extra collision-handling logic. One nuance worth saying out loud: the lesson uses a 301 redirect ("moved permanently"), which lets browsers remember the destination and skip your server on repeat clicks — fantastic for traffic, but it means you can't count those clicks; switch to 302 ("found", temporary) if analytics matter more than raw speed. Naming that trade-off is exactly the kind of judgment a system design interview is checking for.
The framework, in one breath
Almost every design interview rewards the same arc: requirements → API → encoding/data model → read & write paths → scaling. Don't jump to "I'll use Redis" before you've said what the system must do and how big it gets — the requirements are what justify Redis later. Each of the six collapsible steps below is one beat of that arc; expand them in order. The point of writing the numbers down (links/year, requests/second, gigabytes) is that they let you defend every choice instead of hand-waving: "reads are 100× writes, so I optimize the read path with a cache" is a real argument; "I'll add a cache" alone is not.
▶ Step 1 — clarify
Before designing anything, separate functional requirements (the features — what the system does) from non-functional ones (the quality bars — how well it does them: fast, available, durable). Then estimate scale, because the numbers decide the architecture. "Durable" means once a write succeeds it survives crashes; "available" means reads keep working even when parts fail; "p99 <100ms" means 99% of redirects finish in under a tenth of a second.
- Functional: create short URL from long; redirect short → long; expiration optional; custom alias optional.
- Non-functional: high availability on read; redirect <100ms p99; reads ≫ writes (~100:1); writes durable.
- Scale: 100M new links/yr → ~3 writes/sec avg, peak ~30. Reads ~3k/s avg, peak ~30k. 5yr storage ≈ 75 GB.
Back-of-envelope, shown out loud
These numbers aren't memorized — derive them live so the interviewer sees the reasoning. A capacity estimate is just dividing a yearly total by the seconds in a year and multiplying sizes by counts:
- A year has ≈ 31.5M seconds (60 × 60 × 24 × 365). So 100M links/yr ÷ 31.5M ≈ 3 writes/sec on average; assume a peak ~10× the average → ~30 writes/sec.
- Reads are ~100× writes (the read/write ratio from the requirements) → ~300 reads/sec average, ~3k–30k/sec at peak.
- Storage: each row is roughly ~500 bytes (long URL + short code + a little metadata). 100M/yr × 5yr = 500M rows × 500 B ≈ ~250 GB of raw data; with indexes and replicas call it ~75 GB–750 GB depending on assumptions — the headline is "fits comfortably on one machine/cluster," so no sharding yet.
The takeaway that drives the rest: writes are tiny, reads are large, total storage is modest. That trio is why the design leans hard on a read cache and barely worries about the write side.
▶ Step 2 — API
The API is the menu of requests a client can make. Keep it to the two core operations plus delete. POST is the HTTP verb for "create something new" (creating a link is a write); GET is "fetch/go to something" (clicking a link is a read that returns a redirect). A ? after a field below means optional. The server answers the create call with the finished short code; it answers the redirect call with a 301 status whose Location header is the long URL — that header is literally what tells the browser where to go.
POST /shorten { url, custom_alias?, ttl? } → { short }
GET /:short → 301 redirect
DELETE /:short → auth required
ttl ("time to live") is an optional expiry in seconds — after it elapses the link stops resolving. custom_alias lets a user pick their own short code (e.g. bit.ly/my-launch) instead of an auto-generated one. DELETE requires authentication ("auth") so only the link's owner can remove it.
▶ Step 3 — diagram
Now sketch the boxes a request flows through. A load balancer (LB) sits in front and spreads incoming requests across many identical API servers so no one box is overwhelmed. The API servers hold the application logic. The cache (Redis) keeps hot short→long mappings in memory for instant answers. The KV store is the durable source of truth, split into a write primary and one or more read replicas (copies kept in sync, so reads can be served without touching the write side). The CDN edge sits even further out, near users, and can answer popular redirects before the request ever reaches your data center. The "miss" arrow is the fallback path taken only when the cache doesn't have the answer.
CDN edge
|
client → LB → API servers → cache (Redis: short→long)
| |
+----miss────→|
v v
write DB read replica
(KV store) (KV store)
▶ Step 4 — data + IDs
Two decisions here: what to store and how to mint the short code. Storage is a key-value (KV) store — a database whose only job is "value behind this exact key," no joins across tables — because every lookup is exactly "short code → long URL," a single point lookup. ("NoSQL" just means a non-relational database; the KV store is one kind.) The harder question is the encoding scheme for the code itself.
- KV store keyed on
short. Value:{ url, created_at, ttl, owner }. NoSQL: point lookup, no joins. - ID: base62 of central Snowflake counter → ~7 chars covers 62⁷ ≈ 3.5T keys. Alternative:
hash(long)[:7]but collisions need handling. - Custom alias: conditional put (check-and-set) for uniqueness.
Counter + base62 (recommended)
Hand out an ever-increasing unique number (e.g. from a Snowflake-style distributed ID service — a counter that can run on many machines without ever repeating a value), then write that number in base62. Counter value 125 becomes cb; 1,000,000,000 becomes a ~6-char code. No collisions ever — each number is used exactly once — so there's zero collision-handling code. Codes are short and dense.
Random vs hash (the alternatives)
Random: generate 7 random base62 chars and check it's free. No central counter needed (nice for sharding), but as the table fills you collide more often and retry. Hash: run the long URL through a hash and keep the first 7 chars — fast and stateless, but two different URLs can hash to the same code (a collision), so you must detect it and re-hash with a salt. Both trade simplicity for collision logic; counter+base62 avoids that entirely.
Custom aliases bypass the generator, so two users could request the same alias. Guard it with a conditional put (also called check-and-set or compare-and-set): the write only succeeds if the key doesn't already exist, so the database itself enforces uniqueness atomically — no race between "check if free" and "insert."
Why 7 characters is plenty: 62⁷ ≈ 3.5 trillion possible codes. At 100M new links/year you'd burn through 500M in five years — under 0.02% of the space. You could run for centuries before needing an 8th character. This is the moment to show the math rather than assert "it's enough."
▶ Step 5 — deep dive: read path
This is where ~99% of traffic lives, so it deserves the most care. A cache is a fast in-memory store (Redis) that sits in front of the slow disk-backed database; a hit means the answer was already there, a miss means it wasn't and we fall through to the KV store. The reason the cache works so well here is the access pattern: link popularity is Zipfian — a handful of links (a viral tweet, a marketing campaign) soak up the vast majority of clicks. Keep those few hot links in memory and you serve most requests without ever touching disk. TTL here ("time to live") is how long a cached entry stays before it's evicted to make room.
- Cache-first read (Redis), TTL 24h. Hit rate >95% realistic (Zipfian access).
- Miss → KV read → populate cache → return.
- 301 lets browsers cache (great for QPS, bad for analytics). 302 if you need to count.
- Bloom filter at cache layer to short-circuit known-bad lookups.
The redirect step by step
- Request hits an API server (or the CDN edge). Look up the short code in the cache.
- Hit: return immediately with the long URL — the common case, microseconds.
- Miss: read the KV store, copy the answer back into the cache (so the next click is a hit), then return. This "read-through" repopulation is why even a cold link is slow only once.
- Respond with a
301(or302) and aLocationheader pointing at the long URL; the browser then navigates there.
301 vs 302, restated: 301 ("moved permanently") lets the browser and any CDN cache the destination and skip your server on repeat clicks — cheapest for QPS, but you lose per-click analytics. 302 ("found", temporary) forces a hit to your server every time — you can count clicks, at the cost of more load. State the trade and pick based on whether the product needs analytics.
A Bloom filter is a tiny probabilistic membership test ("is this code definitely-not-present, or maybe-present?"). Placed at the cache layer it lets you reject obviously-bogus codes (typos, scans for nonexistent links) without a database round-trip — it can have false positives but never false negatives, so a "definitely not here" answer is trustworthy.
▶ Step 6 — 10× scale
The interviewer multiplies traffic by 10× to see where the design bends. The honest engineering move is to scale the part that's actually under pressure (reads) and resist over-building the parts that aren't (writes, storage). QPS = queries per second. "Scaling out" (horizontal) means adding more machines; "scaling up" (vertical) means a bigger machine — here we scale out the read tier because reads are the load.
- Read 300k QPS: more cache replicas, multi-region; serve redirects directly from CDN edge (Lambda@Edge / Workers).
- Write 300 QPS: trivial; bottleneck is ID service — pre-batch IDs per app server.
- Storage 750 GB: still one cluster shard. No urgent shard need.
- Abuse: rate-limit per IP + per account at gateway; spam-checker async.
Reads (the real problem): 300k QPS won't fit one cache, so add cache replicas (extra copies) and go multi-region. Best of all, push popular redirects to the CDN edge — servers near the user (via Lambda@Edge or Cloudflare Workers) answer the redirect before it ever reaches your data center. With a 301 the browser may not even ask twice.
Writes (a non-problem): 300 writes/sec is trivial for any database. The only sharp edge is the central ID generator becoming a bottleneck; fix it by handing each app server a batch of IDs up front (e.g. 1,000 at a time) so it mints codes locally without asking the central service on every write.
Storage (still fine): ~750 GB fits on one cluster — sharding (splitting the dataset across machines) isn't needed yet. Say so. Adding shards you don't need is a classic over-engineering tell.
Abuse: shorteners attract spam and phishing. Rate-limit (cap requests per time window) per IP and per account at the gateway, and run a spam/malware check asynchronously (after responding, not in the hot path) so it never slows legitimate redirects.
Pitfalls to name out loud
- Collisions: only a worry for hash/random schemes — say you'd detect-and-retry, or sidestep them entirely with counter+base62.
- Hot links: one viral code can hammer a single cache node ("hot key"). Replicate that key across nodes and lean on the CDN/browser cache.
- Custom aliases: risk of duplicates and squatting on nice names; enforce uniqueness with a conditional put and consider a reserved-word blocklist.
Go deeper (optional):
For the canonical write-ups of this exact problem, see the URL-shortener chapters in System Design Interview by Alex Xu and the "Designing a URL Shortening Service" walkthrough on the Grokking the System Design Interview track. Both cover the counter-vs-hash decision and cache sizing in more depth.