System Design · Lesson 6 of 8 · Updated ·
System Design Case Study — Design a URL Shortener (Like bit.ly)
"Design a URL shortener" (like bit.ly or tinyurl) is the most common first system design question. Let's solve it with the 6-step method.
Step 1 — Requirements
Functional
- Given a long URL, return a short one:
sk1.in/aB3xK9q. - Visiting the short URL redirects to the long one.
- Optional: custom aliases, expiry, click analytics.
Non-functional
- Very fast redirects (they're on every click).
- Highly available — a broken short link is a broken link everywhere it was shared.
- Short codes must be unique and not guessable in order.
Step 2 — Estimates
Assume 100 million new URLs per month and 100 reads for every write:
| Quantity | Calculation | Result |
|---|---|---|
| Writes / second | 100,000,000 ÷ (30 × 86,400) | ≈ 39 |
| Reads / second | 39 × 100 | ≈ 3,858 (peak maybe 3–5×) |
| URLs in 5 years | 100 M × 12 × 5 | 6 billion |
| Storage | 6 B × ~500 bytes | ≈ 3 TB |
It's read-heavy → caching matters most.
Step 3 — API
POST /api/urls { "long_url": "https://…", "alias": "optional" } → 201 { "short_url": "https://sk1.in/aB3xK9q" }
GET /{code} → 301/302 redirect to the long URL (404 if unknown)
GET /api/urls/{code}/stats → click counts (optional)
301 vs 302: 301 (permanent) lets browsers cache the redirect — fewer requests to you, but you can't count every click. 302 (temporary) hits your server every time — accurate analytics. Many shorteners use 302 for analytics.
Step 4 — Generating short codes
Use base62 characters: a–z, A–Z, 0–9 = 62 symbols.
| Length | Possible codes |
|---|---|
| 6 | 62⁶ ≈ 56.8 billion |
| 7 | 62⁷ ≈ 3.52 trillion |
7 characters is plenty for 6 billion URLs.
Option A — counter + base62 (recommended): a unique ID generator gives 1, 2, 3, … and we convert the number to base62.
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def to_base62(n):
if n == 0:
return ALPHABET[0]
out = []
while n:
n, r = divmod(n, 62)
out.append(ALPHABET[r])
return "".join(reversed(out))
print(to_base62(125), to_base62(3_844), to_base62(56_800_235_583))
21 100 ZZZZZZ
To avoid one counter becoming a bottleneck, give each server a range of IDs (server 1 gets 1–1,000,000; server 2 the next million…), or use a Snowflake-style ID. To make codes non-sequential, shuffle/encode the ID before converting.
Option B — hash the URL (MD5/SHA) and take 7 characters: simple, but collisions must be checked and handled.
Step 5 — Data model and storage
urls(code PRIMARY KEY, long_url, user_id, created_at, expires_at)
A simple key → value lookup. Either a SQL database (with code as the primary key) or a key-value NoSQL store works; at 3 TB, shard by code (hash sharding) when one machine isn't enough.
Step 6 — Making redirects fast
- Cache hot codes in Redis: popular links get most clicks (the 80/20 rule). Caching 20% of daily reads needs only tens of GB of RAM.
- Read replicas for database reads.
- CDN / edge redirects for the most popular links.
- Track clicks asynchronously: push a click event to a queue; aggregate later.
Trade-offs to mention
- 301 (fast, cached) vs 302 (accurate analytics).
- Counter-based codes (no collisions, needs ID coordination) vs hashing (no coordination, collision checks).
- Abuse prevention: rate limit creation, scan for malware/phishing links.
Quick recap
- Read-heavy: ~39 writes/s vs ~3,858 reads/s; 3 TB over 5 years.
- API: POST to create, GET /{code} to redirect.
- Base62, 7 chars → 3.5 trillion codes; counter ranges or Snowflake IDs.
- Cache hot codes, replicas for reads, async click analytics.