Design a URL Shortener

Our first case-study lesson: apply hashing, sharding, caching, and rate limiting to a real interview classic.

Intermediate · 18 min read

Why this matters

"Design TinyURL" is the interview question that launched a thousand whiteboard sessions. It's also a genuinely good system to design, because it's small enough to hold in your head and big enough to need almost every tool in the distributed-systems drawer: unique ID generation, partitioning, caching, and abuse protection.

This lesson runs like the interview itself. You'll clarify the requirements first (the step everyone skips and everyone regrets skipping), do the back-of-envelope math, then make the hard decisions — starting with the one that makes or breaks this design: how do you mint the short keys?

Video: Design a URL Shortener — System Design Interview (TinyURL & Bitly) — Destination FAANG
Opens with the four follow-ups that break candidates, showing why this question is asked everywhere.

First, clarify the problem

A URL shortener does two things and only two things:

  1. Shorten — accept a long URL, return a short one. POST /shorten with https://example.com/some/absurdly/long/path?with=query&params=too returns https://sho.rt/a3K9xQ2.
  2. Redirect — accept a visit to the short URL and send the browser to the long one with an HTTP redirect.

Everything else is a follow-up question for your interviewer, not a core requirement:

Notice what we did not ask: nothing about frameworks, nothing about cloud providers. Interviewers grade you on decisions and trade-offs, not on brand names.

Video: Ep 1 · Design a URL Shortener — A Full Mock System Design Interview — software-engineer-blog
Mock interview asks four clarifying questions before drawing a single box, modeling the habit to copy.

Picture a coat-check counter

This lesson's running analogy: a coat-check counter at a busy theater. You hand over your bulky coat (the long URL) and the attendant hands you a small numbered ticket (the short key). The coat goes on a rack in the back room (the database). When you come back, you present the ticket and get your coat — the redirect. The mapping, ticket to coat, is the whole system.

Keep the attendant in mind. They're about to have a very bad evening.

Video: What Is a Short Link? — Bitly
Bitly's own explainer defines the short-link swap: long URL in, token out, redirect back.

Back-of-envelope math

Two minutes of arithmetic now saves you from designing for the wrong decade. Start with what the interviewer gave you: 100 million new URLs per month.

Writes: 100,000,000 ÷ (30 × 24 × 3,600 seconds) ≈ 40 new short URLs per second. Call peak traffic 2–3× the average, so size for ~100 writes/sec. A single database could handle that — writes are not your problem.

Reads: you assumed 10 redirects per shortened URL, so ~400 reads/sec on average, ~1,000/sec at peak. Still modest — but reads outnumber writes 10 to 1, which tells you where to spend your engineering: the redirect path deserves the caching budget.

Storage for 10 years: 100M × 12 months × 10 years = 12 billion URLs. Each record holds the long URL (say ~100 bytes on average), the 7-character key, an owner ID and timestamps — call it 500 bytes. 12B × 500 bytes ≈ 6 TB. That fits on a handful of commodity disks, but no single machine should hold the only copy, so you'll shard it.

The theme of this math: nothing here is exotic. The scale is a rounding error for a real database cluster — which is exactly why this problem is about decisions, not heroics.

Video: System Design Interview Course • Back of the Envelop Calculation aka Capacity Estimates — Anubhav Sethi
Works capacity estimation step by step, ending with a URL-shortener example you can reuse.

The key-generation problem

Here's the decision that earns the offer. You need 12 billion unique 7-character tickets, minted across many servers, with no duplicates and no central bottleneck. Three strategies exist. The attendant is about to try all of them.

Strategy 1: Hash the URL. Run the long URL through a hash (say MD5), take the first 7 characters of the base62 encoding. Base62 is a–z, A–Z, 0–9 — 62 symbols, so 7 characters give you 62⁷ ≈ 3.5 trillion possible tickets. Big perk: the same URL always produces the same ticket, so you get free deduplication — shorten the same link twice, get the same key back, store one row. The catch: with billions of keys in a 3.5-trillion keyspace, collisions are not a freak accident, they're a Tuesday. Every insert has to check "is this ticket already taken by a different URL?" and rehash with a salt when it is. Collision checks mean a database read on every write, and the hot path gets slower exactly as you scale.

Strategy 2: Auto-increment counter. Keep one counter in the database, hand out 1, 2, 3… and base62-encode it. Dead simple, never collides, tickets stay short. Two problems. First, the counter is a single choke point: every shorten request on every server queues up for one number — your "distributed" system has a single-file line at the ticket window. Second, the tickets are predictable: anyone can walk sho.rt/1, sho.rt/2, sho.rt/3… and scrape every private link your users ever shortened. (Real shorteners have been bitten by exactly this.) You'd never ship enumerable keys for user content.

Strategy 3: A dedicated key-generation service (KGS). Run a small fleet of servers whose only job is minting tickets — and give each one its own disjoint slice of the keyspace. Server A mints keys only from the first third of the 3.5-trillion-ticket space, server B from the second, and so on; each pre-generates random 7-character keys from its own slice, marks them used in its table, and hands them out. Your app servers fetch batches of, say, 1,000 keys at a time and spend them locally — no coordination, no collision checks, no hot-path database call. Two KGS servers can never mint the same ticket, because their slices never overlap: local bookkeeping plus a fleet-wide partition of the keyspace. (The other correct design is a shared keys table where every claim is atomic — a unique constraint doing the cross-server refereeing. Either way, something fleet-wide has to prevent duplicates; local "used" tables alone never could.) Keys are random, so nobody can enumerate them. The costs: you now operate one more service, and if an app server crashes holding an unused batch, those tickets are wasted. But with 3.5 trillion tickets and 12 billion coats, waste is the cheapest problem on the list.

Interactive diagram: StepThrough (loads in the app)

Video: How Distributed Systems Generate IDs Without a Database (Snowflake IDs) — Packetory
Explains why auto-increment breaks at scale and how Snowflake IDs generate unique, time-ordered keys.

Sharding by key

With the ticket-minting settled, the coats still need rack space. 12 billion rows and 6 TB means one database won't do — so shard by the short key. Hash the 7-character ticket, modulo the number of shards, and each row lives on exactly one shard. Lookups are single- shard reads: the ticket tells you which rack room to walk into, no scatter-gather.

In coat-check terms, the theater got popular and opened ticket counters numbered 1 through 8. Your ticket's number decides which counter holds your coat. Add a ninth counter and rehash — or better, use consistent hashing (you met the ring back in the partitioning lesson) so only a fraction of coats move.

Video: Consistent Hashing | The Backend Engineering Show — Hussein Nasser
Explains modulo hashing limits and consistent hashing for adding shards with minimal remapping.

Remember the 10-to-1 read ratio: the redirect path is where the traffic lives, and a small fraction of links gets most of the clicks — the viral tweet, the product launch. That's the 80/20 rule doing you a favor. Put a cache (Redis, Memcached — pick your fighter) in front of the database and store ticket → long URL with a TTL.

The redirect flow becomes: check the cache, and on a hit, answer in ~1 ms without waking the database. On a miss, read the shard, populate the cache, and redirect. Hot links stay warm in memory; the long tail of forgotten links costs you nothing. And since a redirect is just a lookup, cache invalidation is almost a non-issue — a short URL's destination rarely changes.

Interactive diagram: PacketFlow (loads in the app)

The shorten path above: your POST arrives, the API spends a pre-fetched ticket from its local batch (refilled from the key service in the background — no per-request coordination), and the row lands on the shard its key hashes to. Now the redirect path, where the cache earns its keep:

Interactive diagram: PacketFlow (loads in the app)

Video: Redis use cases for system design: 14 Use Cases — System Design Lab
Covers Redis caching with cache-aside, TTLs, and hot-key handling for read-heavy workloads.

Rate limiting

One loose end, and it's the one the attendant dreads: a spammer scripting shorten requests at 10,000 a minute. Each fake URL burns a ticket, a database row, and rack space — multiplied forever, that's your keyspace and your storage gone to junk.

The fix is rate limiting — capping how many requests a user or IP may make in a window — applied directly: cap shortens per user or per IP (say 50 a day for anonymous users, more for accounts). Picture a bucket per user that refills with tokens at a steady rate: every shorten spends a token, and an empty bucket earns a 429 ("Too Many Requests") until tokens drip back in. That's the token-bucket algorithm, and it's exactly what kills the spammer's 10,000-a-minute script — its bucket empties in seconds and it dies at the door — while legitimate users, spending tokens far slower than they arrive, never notice. (A full rate-limiting lesson comes later in the course; this bucket is all you need for the whiteboard.) You might also require sign-in for bulk shortening — a little friction aimed precisely at the people who deserve it.

A decision tree for the whiteboard, if the interviewer asks you to justify the pick:

flowchart TD
    Q["Need to dedupe identical URLs?"]:::service -->|Yes| H["Hash the URL<br/>+ collision checks"]:::data
    Q -->|No| Q2["More than one app server?"]:::service
    Q2 -->|No| C["Auto-increment counter<br/>simple, unique"]:::data
    Q2 -->|Yes| K["Key-generation service<br/>batches of random keys"]:::service

Video: Rate Limiting Explained: Token Bucket vs Sliding Window | System Design By Microsoft SWE — Mayank Joshi
Compares token bucket, leaky bucket, and sliding window algorithms with clear per-algorithm trade-offs.

Failure modes: the attendant's bad evening

No design survives contact with production without a failure plan. Walk through what breaks:

Video: Design for Failure Explained | Timeouts, Retries, Circuit Breakers & Graceful Degradation — Escoding
Walks through timeouts, retries, circuit breakers, and cascading failures — what breaks and how to survive it.

Interview follow-ups (they will ask)

Once the core design lands, interviewers probe the parked requirements:

Video: I Interviewed 50+ Senior Engineers. 90% Failed This One System Design Question — DynamicInterviewVerse
A Meta interviewer shows the follow-up that sinks 9 in 10 candidates on the URL-shortener question — and the thinking that survives it.

The full architecture

Put it together and the design fits on one diagram — which is the point of the whole exercise:

flowchart LR
    U["Browser"]:::client --> LB["Load balancer"]:::cloud
    LB --> API["API servers<br/>(stateless)"]:::service
    API --> R["Redis cache<br/>(hot links)"]:::data
    API --> K["Key service<br/>(pre-minted keys)"]:::service
    API --> D["DB shards<br/>(by short key)"]:::data
    RL["Rate limiter"]:::security -. "throttles shortens" .-> API

And the redirect itself, down to the HTTP semantics. Use 301 Moved Permanently: it tells the browser "remember this mapping," so repeat visits skip your servers entirely — free caching at the edge. The trade-off is honesty: a 301 means you never see those repeat clicks, so if you ever want analytics, you'd switch to 302 Found and count every visit yourself. Choose 301 for performance, 302 for observability. Don't let anyone tell you it's not a trade-off.

sequenceDiagram
    participant B as Browser
    participant A as API server
    participant C as Redis cache
    participant D as DB shard

    B->>A: GET /a3K9xQ2
    A->>C: GET a3K9xQ2
    alt Cache hit (the hot link)
        C-->>A: long URL
    else Cache miss (the long tail)
        A->>D: SELECT by short key
        D-->>A: long URL
        A->>C: SET a3K9xQ2 (TTL)
    end
    A-->>B: 301 → long URL
    Note over B: Browser caches the 301<br/>and skips you next time

Video: Beginner System Design Interview: Design Bitly w/ a Ex-Meta Staff Engineer — Hello Interview
Ex-Meta staff engineer builds the complete design step by step, from requirements to high-level architecture.

Takeaways

  1. Clarify requirements before designing: shorten + redirect is the core; analytics, custom aliases, and expirations are follow-up questions, not day-one scope.
  2. The key-generation strategy is the crux of this design. A shared counter bottlenecks and produces enumerable keys; hashing gives free dedup but needs collision handling; a dedicated key-generation service — each server minting from its own disjoint slice of the keyspace — handing out batches of random keys is the scalable answer.
  3. Back-of-envelope math (100M URLs/month → ~40 writes/sec, 10:1 reads, 6 TB over 10 years) tells you the redirect path and its cache deserve the engineering budget.
  4. Shard by short key so every lookup is a single-shard read; cache hot links in Redis so the database only serves the long tail.
  5. Rate-limit the shorten endpoint — an unguarded ticket window gets emptied by spammers burning your keyspace and storage.

Check your understanding

  1. 100 million new URLs per month works out to roughly how many shorten requests per second?

    • 400 per second
    • 40 per second
    • 4,000 per second
    • 400,000 per second
  2. Why is a plain auto-increment counter a poor key generator for a public shortener?

    • The keys collide with hashed keys issued by other shorteners
    • Base62 can't encode sequential numbers — you'd need a wider alphabet
    • A counter needs a cache in front of it, and caches are forbidden in interviews
    • Sequential keys let anyone enumerate every shortened link, and the counter is a single bottleneck
  3. In the key-generation-service (KGS) design, what does an app server do when it needs a new short key?

    • It hashes the long URL and checks the database for collisions
    • It asks the KGS fleet to mint one key synchronously, waiting for the reply
    • It takes one from its locally held batch of pre-minted keys, fetching a new batch only when empty
    • It increments a counter stored in Redis
  4. A spammer is scripting 10,000 shorten requests a minute. Your first line of defense is...

    • Rate limiting the shorten endpoint per user/IP
    • Making the short keys longer
    • Adding more database shards
    • Switching redirects from 301 to 302

Go deeper

Want to keep pulling this thread? These talks and tutorials go further than we did here:

Sources & further reading