"Design a URL shortener" is the question almost every system design interview opens with — and that's exactly why it's worth nailing. It looks trivial ("it's just a hashmap"), but the follow-ups are where candidates fall apart: how do you generate a short code with no collisions at a billion URLs? How do you serve 120,000 redirects a second without melting the database? 301 or 302?
This is the condensed walkthrough; the full guide (every step, all three key-generation approaches, the Kafka analytics path, and the full production .NET 9 code) is on my site 👇
Full guide: https://prepstack.co.in/blog/design-a-url-shortener-system-design
The design at a glance
| Concern | Decision |
|---|---|
| Short code | 7-char base62 → ~3.5 trillion combinations |
| Key generation | Key Generation Service (KGS) — pre-generate unique keys, hand them out |
| Storage | Key-value store (shortCode → longUrl), sharded by shortCode
|
| Read path | Cache-first (Redis) — 100:1 read:write means the cache does the work |
| Redirect | 301 for performance, 302 if you need per-click analytics |
| Scale | ~1,200 writes/sec, ~120,000 redirects/sec, ~90 TB over 5 years |
Estimates decide everything
100M new URLs/day, 100:1 read:write:
Writes: 100,000,000 / 86,400 ≈ 1,160/sec (~1.2k/sec — trivial)
Reads: 100x writes ≈ 116,000/sec (~120k/sec)
Peak (≈2–3x) ≈ ~300k/sec
Storage (5 yr): ~180B URLs × ~500 B ≈ ~90 TB
Two takeaways: writes are trivial, and ~120k reads/sec is what forces caching and replication. The redirect path must almost never touch disk.
Why 7 chars? 62^6 ≈ 57B (too few for ~180B URLs); 62^7 ≈ 3.5 trillion (plenty). So 7 it is.
The heart of the problem: generating the short code
Approach A — hash the URL. Truncate MD5/SHA-256 to 7 base62 chars. Problem: truncation will collide, forcing a DB check + re-hash on every write (stateful, slow), and identical URLs collide to the same code. Avoid.
Approach B — counter + base62. Each URL gets the next integer, base62-encoded. Zero collisions by construction, but a single counter is a bottleneck + SPOF, and sequential codes are guessable. Fix the bottleneck by handing out ranges (a coordinator gives each server a block of 10,000).
Approach C — Key Generation Service (what to ship). Pre-generate unique 7-char keys offline into a pool split into unused/used. On a write, grab an unused key and mark it used — O(1), no hashing, no collision check on the request path. Each server checks out a block into memory so it rarely hits the KGS. Run a standby replica; losing an in-memory block is harmless (3.5T keyspace).
The read path is cache-first
Client → LB → stateless app servers
│
[ Redis ] ── hit (~90%+) ──▶ 301/302
│ miss
▼
[ KV store ] ─▶ populate cache ─▶ redirect
Link popularity is heavily skewed, so an LRU Redis cache holding the hot links absorbs the vast majority of the 120k reads/sec. Shard the KV store by shortCode — it's uniformly distributed, so no hot shards.
301 vs 302 — a real trade-off
301 (permanent) lets the browser cache the redirect: subsequent clicks skip your server entirely (fast, cheap, but you lose per-click analytics). 302 (temporary) routes every click through you: full analytics and a changeable target, at the cost of more load. Analytics-driven shorteners lean 302.
Tracking clicks without slowing the redirect
If you chose 302 and analytics matter, never write to an analytics DB on the redirect path. Fire-and-forget onto a queue and return immediately:
GET /{shortCode}
→ look up longUrl (cache) ~1–2ms
→ emit click event to Kafka fire-and-forget
→ 302 redirect returned immediately
[ Kafka ] → [ workers ] → [ analytics store ]
I shipped this in production
Our report-sharing feature first embedded a 36-char sequential-GUID primary key in the share URL — long, ugly, enumerable, and a SELECT against Azure SQL on every open. Rebuilt as exactly this design (KGS handing out 7-char base62 codes by the block, cache-first .NET 9 endpoint):
| Metric | Before | After |
|---|---|---|
| Share-link format | 36-char sequential GUID | 7-char base62 |
| Guessability | Enumerable | Crypto-random, ~3.5T keyspace |
| Redirect data source | Azure SQL every open | Redis cache-first, SQL only on miss |
| Cache hit rate | none | ~97% |
| Redirect p95 | ~180 ms | ~5 ms |
The concurrency story is one SQL statement — UPDATE TOP (@blockSize) ... OUTPUT inserted.Code WITH (UPDLOCK, READPAST) — so many app servers check out disjoint blocks with no distributed lock, then serve each block from an in-memory queue. (Full KeyGenerationService + cache-first minimal-API redirect code is in the post.)
The model to carry forward
A URL shortener is a key-generation problem stapled to a caching problem. Three habits it teaches: (1) let the read:write ratio pick your architecture — 100:1 means "cache," full stop; (2) generate uniqueness ahead of time — pre-computing keys turns a collision problem into an O(1) hand-out; (3) name the 301 vs 302 trade-off.
The full guide has all three approaches in depth, the base62 code, custom-alias handling, the capacity table, the design checklist, the full production .NET 9 implementation, and the "when this is overkill" honest section:
https://prepstack.co.in/blog/design-a-url-shortener-system-design
Originally published on PrepStack.
Top comments (0)