← All posts
general

Design a URL Shortener: System Design Walkthrough

"Design TinyURL" is the classic warm-up system design question — small enough to finish in 35 minutes, rich enough to expose whether you reason about scale or recite buzzwords. It's a favourite for early-senior loops precisely because the simplest correct answer is nearly trivial, so everything you add must be justified. That justification is the interview.

New to the format? The system design framework and prerequisites cover the method this walkthrough applies.

Requirements and numbers

Functional: shorten a long URL to a compact code; redirect visitors; optional custom aliases and expiry; basic click analytics.

Non-functional: redirects must be fast (<50ms) and highly available — a broken redirect breaks someone else's page. Assume 100M new URLs/month (~40 writes/s) against 10B redirects/month (~4k reads/s): a 100:1 read-to-write ratio. Say this ratio out loud; every design decision that follows hangs off it.

The short code

A 7-character code over base62 (a–z, A–Z, 0–9) gives 62⁷ ≈ 3.5 trillion combinations — decades of headroom at our write rate. The generation strategies, in the order interviewers expect you to weigh them:

  • Hash the URL (MD5/SHA, take 7 chars): deterministic, but collisions need handling and the same URL always maps to one code — a problem when two users want separate analytics for the same target.
  • Random + uniqueness check: simple, but every write needs a read-before-insert, and retries cluster as the space fills.
  • Counter + base62-encode: collision-free by construction and trivially fast. A global counter is a single point of contention, so shard it: each application server leases a range of IDs (say, 1M at a time) from a coordination service and hands them out locally. This is the answer most interviewers are steering toward — mention that sequential codes are guessable and can be scrambled (bijective permutation) if enumeration matters.

Storage

The data model is one small row: code → {long_url, owner, created_at, expiry, click_count}. At ~500 bytes/URL and 1.2B URLs/year, that's ~600GB/year — modest. The scaling pressure is read QPS, not volume:

  • A single relational instance handles the write load easily; reads want a replicated or partitioned key-value store, with the code as the natural partition key.
  • Either answer (sharded SQL vs. Dynamo/Cassandra-style KV) is defensible if you name the trade: relational buys custom-alias uniqueness constraints and easy per-user queries; KV buys effortless horizontal reads. What's not defensible is choosing by fashion.

The redirect path — where the design lives

At 4k reads/s with a hot-head distribution (a viral link is a hot key by definition), the redirect path is cache-shaped:

  1. CDN/edge: redirects are cacheable HTTP responses; a popular code can be answered without touching your origin at all.
  2. Application cache (Redis/Memcached, cache-aside, LRU): even a 20% cache holding the hot head serves the vast majority of reads — Zipf distributions are your friend; say so.
  3. Store lookup on miss, then populate.

301 vs 302 is a classic probe: 301 (permanent) lets browsers cache the redirect and never return — great for load, fatal for analytics and for expiring links. 302/307 keeps every click observable. Pick 302 because of the analytics requirement, and name the cost.

Missing codes deserve a sentence: random probes for non-existent codes bypass every cache layer by design. A Bloom filter over issued codes in front of the store turns that attack into a no-op.

Analytics, expiry, and abuse

  • Clicks: never write synchronously on the redirect path. Emit an event to a queue; aggregate asynchronously. The redirect returns before the analytics system has heard about it.
  • Expiry: lazy-delete on access plus a background sweep — nobody needs an expired link to vanish at the exact second.
  • Abuse: shorteners are phishing infrastructure by default. Check destinations against threat feeds at creation, and per-user rate limiting at the write API.

How this plays in an interview

A clean 35-minute pass: requirements with the 100:1 ratio (5 min) → code generation with the three options weighed (10) → storage model and choice (5) → the cached redirect path with 301/302 reasoning (10) → analytics/expiry/abuse (5). The candidates who fail this question fail it by over-building — twelve microservices for what is, at heart, a hashmap with a cache. Restraint, justified numerically, is the senior signal.


Next up in difficulty: design a rate limiter, or jump to the full-scale case studies — WhatsApp, Uber, Netflix. Want live practice? Get matched with a coach for mock system design rounds.