Design a URL Shortener: Estimates, ID Generation, Redirects and Scale
The URL shortener is the "hello world" of system design interviews. Everyone knows the shape, which is exactly why interviewers use it: the difference between candidates shows up in ID generation, the redirect choice and the numbers, not in the boxes.
Requirements
Functional
- Create a short link for a long URL, optionally with a custom alias.
- Redirect a short link to the original URL.
- Optional: expiry dates and basic click counts.
Non-functional
- Redirects must be fast (they sit in front of every click) and highly available.
- Short codes must be unique and should not be guessable in sequence if links can be private.
- Reads hugely outnumber writes.
Out of scope unless asked: user accounts, detailed analytics dashboards, link previews.
Estimates
Assume 100 million new links per month and a 100:1 read/write ratio.
- Writes: 100M ÷ (30 × 86,400 s) ≈ 40 per second.
- Reads: ≈ 4,000 redirects per second on average, ~10,000 at peak.
- Storage: 100M × 12 months × 5 years = 6 billion links. At ~500 bytes each (URL plus metadata) that's about 3 TB.
What it means: writes are tiny, the read path needs caching, and 3 TB fits a partitioned key-value store comfortably.
API
POST /links { longUrl, customAlias?, expiresAt? } -> { shortCode, shortUrl }
GET /{shortCode} -> 301 or 302 redirect to longUrl
Data model
One table keyed by the short code:
| Field | Notes |
|---|---|
short_code (key) | 7 characters, base62 |
long_url | Up to a few KB |
created_at, expires_at | For expiry |
owner_id | Optional |
The only hot query is "look up by short code", so a key-value store (or any database with a primary-key lookup) fits well.
Generating short codes
How long does the code need to be? With base62 (a–z, A–Z, 0–9):
- 6 characters → 62⁶ ≈ 56.8 billion codes.
- 7 characters → 62⁷ ≈ 3.5 trillion codes.
Seven characters leaves plenty of room for 6 billion links. Now, three ways to produce them:
| Approach | How | Pros | Cons |
|---|---|---|---|
| Hash and truncate | Hash the long URL (e.g. MD5), base62-encode, take 7 chars | Same URL can map to the same code | Collisions must be detected and retried; truncation makes them more likely |
| Counter + base62 | A unique number per link, encoded in base62 | No collisions, simple | A single counter is a bottleneck; sequential codes are guessable |
| Random + check | Generate 7 random base62 chars, insert only if absent | Unguessable, no coordination | Needs a uniqueness check (a conditional insert) on every write |
A strong answer for scale: counter ranges. A small coordination service hands each app server a block of IDs (say 1,000,000 at a time); servers allocate from their block locally, with no per-request coordination. If guessability matters, shuffle the number with a reversible permutation before encoding, or use the random approach.
Custom aliases go through the same uniqueness check as random codes.
The redirect: 301 or 302?
- 301 (permanent): browsers cache the redirect, so repeat clicks may never reach your servers. Less load, but you lose click counts and can't change the destination later.
- 302 (temporary): every click comes back to you. More load, but accurate analytics and editable links.
Choose based on the requirements and say the trade-off. If analytics matter, 302.
High-level design
- Write path: client → API servers → ID allocation → write to the key-value store.
- Read path: client → load balancer → redirect service → cache → store on a miss → redirect response.
A small fraction of links gets most of the clicks, so a cache (Redis, or a CDN for the very hottest links) absorbs most redirects. Cache-aside with an LRU policy works well; link targets rarely change.
Scaling and follow-ups
- Partitioning: shard the store by short code (hash-based), which spreads load evenly because codes are effectively random.
- Click analytics: don't write a row synchronously on every redirect. Emit an event to a queue and aggregate asynchronously.
- Expiry: check
expires_aton read; clean up expired rows in a background job. - Abuse: rate limit creation per user or IP, and check new URLs against a malware/phishing list.
- Availability: replicate the store across zones; the redirect path should keep working if the write path is down.
Common mistakes
- No estimates, so no justification for the cache or the storage choice.
- Hashing the URL without handling collisions.
- A single global counter with no plan for its throughput or failure.
- Never mentioning 301 versus 302.
- Writing analytics synchronously in the redirect path.
Practise it step by step. Prepd's URL shortener question walks through requirements, capacity estimates, the data model, the architecture and the API as separate steps, with written feedback on each answer.