Module SD18

Design a URL Shortener

Lookup-heavy, ID generation, redirects, and analytics.

By the end of this module you can…
  • 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: POST a 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

ApproachProblem
Hash(URL), take first 7 charsCollisions need a check-and-retry loop; identical URLs collide (maybe fine, maybe not).
Random 7-char string, check-and-retryFine at low fill; the collision check is a DB round trip per create.
Global counter → base-62 encodeNo collisions ever, but the counter is a write bottleneck and a SPOF; codes are sequential and enumerable.
Counter + range allocation + bijective scrambleNone of the above: no collision check, no hot counter, codes not enumerable. The recommended answer.
TEXT
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 a
bijective function keyed by a secret: a small Feistel network over the
id space. Reversible, collision-free, unguessable.
Not "multiply by a large coprime mod 62^7": consecutive ids then give codes
that differ by the same constant, so two links created back to back reveal
the step and the whole space can be walked. It hides the order; it doesn't
hide the sequence.

Data model & storage

TEXT
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, cached
custom_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

POSTGET /{code}claim rangewritelookupmissclick eventClientLB / CDNCreate APIRedirect svcTicket serverid rangesLinks DBCachecode→urlClick queueAnalytics
Reads (redirects) are served from cache with a huge hit rate — hot links may also be cached as a 302 at the CDN. Clicks are fired to a queue; the redirect returns immediately.

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 short Cache-Control if analytics matter; 301 if 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 302 for 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.
⚠Over-engineering is the trap here
The write rate is ~40/sec. If you propose Kafka, a Cassandra cluster, and six microservices, a good interviewer marks you down. The Staff+ answer is: “a single Postgres with read replicas, Redis in front of redirects, a ticket server for ids, and a queue only for the click events. That's it. Here's where it would need to change if we hit 100×.”

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.
Recap — burn these in
  • 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.
Next · Mock interview · 1 drillRun it as a 45-minute mock interview against an AI interviewer.
What the course does between lessons

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.

Review · try it
card 1 / 5

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.

Today

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.

Problem → pattern
  • “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 →

The rest of it

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