Skills101

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.

URL shortener: browser → load balancer → API servers, which use an ID generator (base62), a cache of hot short codes and a database mapping code to URL; estimates: 100 million new URLs a month, about 39 writes and 3,858 reads per second, 3 TB over 5 years, 62⁷ ≈ 3.52 trillion codes

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:

QuantityCalculationResult
Writes / second100,000,000 ÷ (30 × 86,400)≈ 39
Reads / second39 × 100≈ 3,858 (peak maybe 3–5×)
URLs in 5 years100 M × 12 × 56 billion
Storage6 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.

LengthPossible codes
662⁶ ≈ 56.8 billion
762⁷ ≈ 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.

Practice what you learned

📝 Quiz: Design a URL Shortener5 questions · instant answers & explanations · +10 XP · counts towards your free certificateTake the quiz →