Design a URL Shortener
Lookup-heavy, ID generation, redirects, and analytics.
- Generate short codes with no collisions and no guessable sequence
- Serve redirects at massive read scale from cache
- Keep analytics off the redirect hot path
- Resist the urge to over-build a fundamentally small system
The crux
It looks trivial, and the write side is — a few dozen creations per second. The interesting parts: (1) generating unique short codes at scale without collision checks becoming a bottleneck or the codes being enumerable, (2) serving redirects — which are 100–1000× the write volume and must be fast and always up — and (3) doing analytics without slowing redirects down. And, at Staff+, noticing that this is a small system and not reaching for Kafka and Cassandra.
Requirements
- Functional:
POSTa long URL → get a short code;GET /{code}→ redirect; optional custom alias; optional expiry; basic click analytics (count, referrer, country over time). - Scale: ~100M links created/month (~40/sec avg, ~200 peak); ~10B redirects/month (~4K/sec avg, tens of thousands peak, with viral spikes far higher).
- Non-functional: redirect p99 < 50 ms; redirects 99.99% available (a dead link breaks someone else's content); new links work within ~1 s; links effectively live forever.
Estimation highlights
100M/month × 60 months × ~500 B ≈ 3 TB over five years — tiny, fits one beefy database with replicas. Keyspace: 5 years ≈ 6B links; base-62 gives 62⁶ ≈ 57B (tight), 62⁷ ≈ 3.5T (comfortable) → 7-character codes. The redirect read rate is the only big number, and it's a cache problem.
Short-code generation
| Approach | Problem |
|---|---|
| Hash(URL), take first 7 chars | Collisions need a check-and-retry loop; identical URLs collide (maybe fine, maybe not). |
| Random 7-char string, check-and-retry | Fine at low fill; the collision check is a DB round trip per create. |
| Global counter → base-62 encode | No collisions ever, but the counter is a write bottleneck and a SPOF; codes are sequential and enumerable. |
| Counter + range allocation + bijective scramble | None of the above: no collision check, no hot counter, codes not enumerable. The recommended answer. |
Range allocation:Each app server claims a block of 10,000 ids from a "ticket server"(one row: SELECT next_range FOR UPDATE; next_range += 10000).It hands out ids locally with no coordination until the block runs out.-> The ticket server sees ~1 request per 10,000 creates. Not a bottleneck.-> An app server crash wastes at most 10,000 ids (out of trillions). Fine.Anti-enumeration:Don't base-62 the raw id (12345 -> "3d7"). Scramble it first with abijective function keyed by a secret: a small Feistel network over theid space. Reversible, collision-free, unguessable.Not "multiply by a large coprime mod 62^7": consecutive ids then give codesthat differ by the same constant, so two links created back to back revealthe step and the whole space can be walked. It hides the order; it doesn'thide the sequence.
Data model & storage
links(code PK, long_url, owner_id, created_at, expires_at)read pattern: get by code, ~4K+/sec, must be <10ms -> KV store or Postgres, cachedcustom_aliases: same table, code = the chosen alias, uniqueness = the PK constraint
A KV store (DynamoDB, Cassandra) keyed by code gives O(1) reads at any scale; Postgres is also fine at 3 TB with read replicas. Say either, and note the redirect path is really served from cache.
High-level design
Deep dive: the redirect path
- Cache hit rate is the whole game. A tiny fraction of links get almost all the traffic; a Redis tier holding the hot set gets >99% hits, so the DB sees a trickle.
- 301 vs 302.
301(permanent) is cached by the browser — the second click never reaches you, so you lose analytics and the ability to change or disable the link.302(found) means every click hits you. Use 302 with a shortCache-Controlif analytics matter;301if they don't and you want maximum offload. - Viral link. One code getting 500K redirects/sec melts its single cache shard. Fixes: replicate that key across cache nodes; let the CDN cache the
302for a few seconds; a per-node local micro-cache with a 1 s TTL absorbs almost all of it. - Analytics off the path. Emit a
{code, ts, referrer, ip}event to a queue and return the redirect. An async pipeline aggregates into time-bucketed counters. If the queue is down, drop the event — a redirect must never fail or slow for analytics.
Staff+ angles
- Multi-region / data residency: allocate id ranges per-region (or region-prefix the code) so codes stay globally unique without coordination; geo-replicate the links DB read-only; serve redirects from the nearest region.
- Abuse: run new URLs through a safe-browsing / malware check async; rate-limit creation per account; allow takedowns (a code → disabled state served from cache).
- Cost: this system is cheap — the honest Staff+ point is that engineering effort should go to analytics and abuse, not the core.
- Codes: counter + per-server range allocation (ticket server sees 1 req / 10K creates) + a bijective scramble so they're not enumerable. No collision checks.
- Storage is tiny (~TBs). Reads (redirects) are the only big number and they're a cache problem — hot links get >99% hit rate; the CDN can cache the 302.
- 302 + short cache if you want analytics; 301 for maximum offload. Never write analytics synchronously on the redirect path — fire an event to a queue.
- Viral link = hot key: replicate the cache entry, CDN-cache the redirect, per-node micro-cache.
- Don't over-build. A Postgres + Redis + ticket server + click queue is the whole system at this scale.
The part an AI chat can't give you
An LLM will happily explain a monotonic stack — and you should let it. What it won't do is decide what you learn next, remember that you got something wrong three weeks ago, or put it back in front of you today. That machinery is the course.
Think in half-open ranges [i,j); no …
Real cards from the two free modules, scheduled by the same SM-2 code the course runs. Every module's key points become cards.
Review 12 cards, then continue 14 Backtracking.
The dashboard picks the one next thing, so you never open the course and wonder what to do.
- “sorted” + “find a pair”→Two pointers
- “longest substring…”→Sliding window
- “next greater element”→Monotonic stack
- “top k” / “k closest”→Heap
- “number of ways”→DP over states
Two full indexes map the phrases you actually see in a problem to the structure or building block they imply. Browse the DSA one free →
That's 3 of 81.
The rest of Data Structures & Algorithms, all of Broader CS, and the full System Design track — including 19 case studies you run as mock interviews — plus every exercise and the spaced-repetition review, are one payment away.
Already bought it? Unlock