Design a URL Shortener

Work the classic interview question end to end: requirements, estimates, API, data model, the design, short-code generation, caching and failure.

Case study32 minLesson 1 of 7
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

You paste a long link and get back something like sho.rt/008sudj. Anyone who opens the short link lands on the long one. It looks like a weekend project, which is why interviewers like it: a small surface hides a read-heavy workload, an ID scheme that has to work across many servers, a cache that does most of the work, and a database that must never lose a row.

This case study follows the same steps as A Framework for Any Design. Try each "Your turn" box before you read on. The point is to practise the thinking, not to memorise this answer.

Clarify the problem

The prompt is one sentence on purpose. The interviewer wants to see whether you pin down the scope before drawing boxes. Ask about features first, then scale, then what matters most when things go wrong.

The first five minutes
Interviewer

Design a URL shortener.

You

Let me check the scope first. Users submit a long URL and get a short one, and opening the short one redirects. Can users pick a custom alias? Do links expire? Do we need click counts?

Interviewer

Custom aliases, yes. Links live forever unless the user sets an expiry. A click count per link is enough; no dashboards.

You

How many links are created, and how often is each one opened?

Interviewer

Around 100 million new links a month. Reads are about 100 times the writes.

You

Then this is read-heavy, and the redirect is the path to protect. I'll aim for redirects that add under 20 ms on our side and stay up even when link creation is down. Losing a link once created is not acceptable. I'll leave out accounts, spam scanning and analytics dashboards unless you want them.

Write the result down where the interviewer can see it:

  • Functional: create a short link for a long URL, with an optional custom alias and expiry; redirect a short code to its long URL; count clicks per link.
  • Non-functional: redirects are fast (under 20 ms added) and highly available; created links are never lost; codes are short.
  • Out of scope: user accounts, abuse scanning, analytics dashboards.

Estimate the scale

Turn the interviewer's numbers into per-second rates and bytes. A month has about 30 × 86,400 ≈ 2.6 million seconds.

  • Writes: 100 million ÷ 2.6 million ≈ 40 links a second on average. Assume peaks of 5x: 200 a second.
  • Reads: 100 × 40 = 4,000 redirects a second on average, 20,000 at peak.
  • Code length: with 62 characters (0-9, a-z, A-Z), 6 characters give 626 ≈ 57 billion codes and 7 give 627 ≈ 3.5 trillion. Five years of links is 6 billion, so 7 characters leave plenty of room.
Estimate: Storage for five years

How much storage do the link records need after five years?

You know
  • 100 million new links a month
  • About 500 bytes per record (code, long URL, timestamps)
  • Nothing is deleted

Show the working
  1. 100 million × 12 months × 5 years = 6 billion links
  2. 6 × 109 × 500 B = 3 × 1012 B = 3 TB
  3. With 3 copies for durability: about 9 TB of disk

Estimate 3 TB (anything from 1 TB to 10 TB is a good answer)

3 TB is more than you want on one machine, but small for a cluster. It points at partitioning later, not on day one.

Estimate: Average read rate

About how many redirects per second must the system serve on average?

You know
  • 100 million new links a month
  • 100 reads for every write
  • A month is about 2.6 million seconds

Show the working
  1. Reads per month: 100 million × 100 = 10 billion
  2. 10 × 109 ÷ 2.6 × 106 s ≈ 3,900, call it 4,000 a second

Estimate 4 thousand/s (anything from 2 thousand/s to 8 thousand/s is a good answer)

Two more numbers shape the design. Each redirect response is about 500 bytes, so even 20,000 a second is only 10 MB/s of traffic. And if the hottest 10 million links get most clicks, caching them takes 107 × 500 B = 5 GB of memory, which fits on one cache node.

Your turn: Define the API

Write the two endpoints before reading on: method, path, request body, and what each returns, including the error cases.

Compare with a model answer

A create endpoint (POST /api/links with the long URL and optional alias and expiry, returning the code) and the redirect itself (GET /{code}, returning a redirect status with a Location header). The interesting choice is which redirect status, covered next.

Define the API

HTTP API
POST /api/links
  body: { "long_url": "https://...", "alias": "spring-sale"?, "expires_at": "2027-01-01"? }
  201 Created   { "code": "008sudj", "short_url": "https://sho.rt/008sudj" }
  400 Bad Request   the URL is not valid
  409 Conflict      the alias is taken
 
GET /{code}
  302 Found   Location: https://the-long-url...
  404 Not Found     no such code, or it has expired

The redirect status is a real decision. A 301 (moved permanently) lets browsers cache the redirect, so repeat clicks never reach you: less load, but you cannot count those clicks or change the target later. A 302 (found) is not cached, so every click comes through. We need click counts, so use 302 and accept the extra traffic, which the estimate says is small.

Protect POST with a per-client rate limit; a shortener without one becomes a free tool for spammers within days.

Your turn: Model the data

What does one record hold, how is it looked up, and which kind of database fits that access pattern?

Compare with a model answer

One record per code. Every read is "give me the row for this code", every write is an insert, and nothing joins. That is a key-value access pattern.

Model the data

links
code        CHAR(7)   PRIMARY KEY   -- "008sudj" or a custom alias
long_url    TEXT                    -- up to 2 KB
created_at  TIMESTAMP
expires_at  TIMESTAMP NULL
clicks      BIGINT                  -- updated in batches, not per click

The only query on the hot path is a lookup by primary key. There are no joins, no range scans, and rows never change after they are written (except the click count). A key-value or wide-column store (such as DynamoDB or Cassandra) fits this well: it partitions by key out of the box, which we will want at 3 TB and beyond. A relational database also works at this size, with read replicas; say so, and say you would pick it if the team already runs one. What matters is the reason: single-key reads, no joins, easy partitioning by code.

Custom aliases live in the same table. Because code is the primary key, "insert if absent" makes alias claims safe: two users racing for spring-sale cannot both win.

Your turn: High-level design

Sketch the boxes for both paths: what sits between a browser and the database on a redirect, and where does a new code come from?

Compare with a model answer

Split the read path from the write path so each scales on its own: a stateless redirect service with a cache in front of the store, and a create service that hands out codes. The next section draws it.

High-level design

Redirects outnumber creates 100 to 1, so they get their own service and their own cache. Creating a link needs a code that no other server will hand out; a small ID allocator gives each create server a block of numbers to use.

Reads and writes take separate paths. The redirect service reads through a cache; the create service takes IDs from a block reserved with the allocator.Browser or app to Load balancer (HTTPS); Load balancer to Redirect service (GET /{code}); Load balancer to Create service (POST /links); Redirect service to Link cache (get code); Redirect service to Links store (on miss); Create service to Links store (insert); Create service to ID allocator (next ID block).Browser or appLoad balancerRedirect serviceCreate serviceLink cacheLinks storeID allocatorHTTPSGET /{code}POST /linksget codeon missinsertnext ID block
Reads and writes take separate paths. The redirect service reads through a cache; the create service takes IDs from a block reserved with the allocator.

Step through creating a link, then opening it twice.

Write path, then read pathUse the arrows or ← → keys
  1. Write path, then read path, step 1Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cacheLinks storeID allocator

    1A user submits a long URL. The load balancer sends POST /api/links to any create server; they are all alike.

  2. Write path, then read path, step 2Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceid 125,000,017Link cacheLinks storeID allocator

    2This server reserved IDs 125,000,000 to 125,000,999 from the allocator earlier, so it takes the next one without asking anyone: 125,000,017.

  3. Write path, then read path, step 3Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate service008sudjLink cacheLinks storeID allocator
    code
    008sudj

    3It writes the number in base 62 and pads it to 7 characters: 008sudj.

  4. Write path, then read path, step 4Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cacheLinks storeinsert 008sudjID allocator
    code
    008sudj

    4It inserts the row into the links store. The store keeps three copies before saying yes, so the link survives a disk failure.

  5. Write path, then read path, step 5Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cacheLinks storeID allocator
    code
    008sudj

    5The short URL goes back to the user. One store write, no other coordination.

  6. Write path, then read path, step 6Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cacheLinks storeID allocator
    code
    008sudj

    6Someone opens sho.rt/008sudj. The load balancer sends the GET to a redirect server.

  7. Write path, then read path, step 7Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cachemissLinks storeID allocator
    code
    008sudj

    7The redirect server asks the cache. The link is new, so it is a miss.

  8. Write path, then read path, step 8Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cacheset 008sudjLinks storeID allocator
    code
    008sudj

    8It reads the row from the store by primary key, then puts it in the cache. Links never change, so the cached copy cannot go stale; only deletes and expiry need care.

  9. Write path, then read path, step 9Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cacheLinks storeID allocator
    code
    008sudj
    status
    302

    9The browser gets 302 Found with the long URL in Location, and follows it.

  10. Write path, then read path, step 10Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceLink cachehitLinks storeID allocator
    code
    008sudj
    status
    302

    10The next click is a cache hit. Most redirects end here and never touch the store, which is why the store can stay small.

  11. Write path, then read path, step 11Browser or app to Load balancer; Load balancer to Redirect service; Load balancer to Create service; Redirect service to Link cache; Redirect service to Links store; Create service to Links store; Create service to ID allocator.Browser or appLoad balancerRedirect serviceCreate serviceIDs left: 982Link cacheLinks storeID allocatordown

    11If the allocator goes down, create servers keep using the IDs left in their blocks. At 40 writes a second spread over 10 servers, a block of 1,000 lasts about 4 minutes: time to fail over.

1 / 11

Deep dives

Interviewers push on two or three parts. For this question it is almost always how codes are made, how redirects stay fast, and how clicks are counted without slowing redirects.

Generating short codes

Every server must hand out codes that no other server hands out, without a slow check on each request.

Ways to make a short code
OptionGood atBad atPick it when
Hash the long URLThe same URL always gets the same code, so duplicates are free to detect.Taking 7 characters of a hash collides; you must check and re-hash. Two users shortening the same URL share one code, and one link's expiry affects the other.Deduplication matters more than anything else, which is rare.
Random code, retry on collisionNo coordination at all. Codes are not guessable.Each create needs an insert-if-absent and the occasional retry. The collision rate grows as the table fills.Codes must not be guessable (private documents, unlisted links).
Counter blocks + base 62 Usual defaultNo collisions, ever. Each server talks to the allocator once per thousand links.Codes are sequential, so anyone can walk through them. Needs a small, highly available allocator.Public links where guessability is fine. A good default.

The arithmetic behind the random option: with 6 billion links stored and 3.5 trillion possible codes, a new random code collides with probability 6 × 109 ÷ 3.5 × 1012 ≈ 0.2%. One retry in 500 creates is fine. If an interviewer worries about guessable codes from the counter, keep the counter and scramble the number with a reversible function before encoding: still no collisions, no longer sequential.

Keeping redirects fast

Clicks are lopsided: a few links get most of the traffic. A cache in front of the store, with LRU eviction, serves most redirects in about a millisecond. Because a link never changes after it is created, there is no invalidation problem on writes. Deletes and expiry are the exceptions: delete the cache key when a link is deleted, and check expires_at on every redirect so an expired link is never served from cache. The caching lesson covers stampedes, which matter here when a link goes viral.

Counting clicks without slowing redirects

Updating a counter row on every click would put a write on the hot path, and a viral link would make one row the hottest spot in the database. Instead, the redirect service publishes a small click event to a queue and returns immediately. A worker reads the events, adds them up per code, and writes one update per link every few seconds.

Clicks leave the redirect path as events. A worker batches them, so a link clicked 5,000 times in a minute costs a handful of writes.Redirect service to Click events (click 008sudj), asynchronous; Click events to Counter worker, asynchronous; Counter worker to Links store (clicks += 312).Redirect serviceClick eventsCounter workerLinks storeclick 008sudjclicks += 312

request and replyasynchronous message

Clicks leave the redirect path as events. A worker batches them, so a link clicked 5,000 times in a minute costs a handful of writes.

The trade-off to say out loud: counts lag by a few seconds, and if the queue loses events, counts are slightly low. For click counts that is fine. For billing per click it would not be, and you would make the events durable and processing idempotent.

Bottlenecks and failures

At 10x the traffic (200,000 redirects a second at peak, 30 TB over five years), the redirect servers scale by adding more, since they hold no state. The cache becomes a cluster, split by consistent hashing so adding a node moves only a small share of keys. The store is partitioned by code, which spreads evenly because codes are spread evenly. The allocator barely notices: blocks of 1,000 at 2,000 writes a second is 2 requests a second.

When this diesWhat happensWhat you do
A redirect or create serverRequests to it fail until the load balancer notices.Health checks remove it in seconds; the others absorb its share.
A cache nodeIts keys miss and fall through to the store.Size the store for the extra reads, or run cache replicas.
A store nodeIts partition loses one of three copies.Reads and writes continue on the other replicas; the node is rebuilt from them.
The ID allocatorNo new blocks are handed out.Servers live on their blocks; run the allocator on three nodes with a consensus store.
The click queueCounts stop updating.Redirects are unaffected, which is the point of making counting asynchronous.

What a strong answer sounds like

Every level gets to a working design. What differs is how much of it is driven by the numbers and how the trade-offs are argued.

LevelWhat the answer does
JuniorDraws a service and a database, hashes the URL for a code, and gets a working path once prompted about collisions.
MidEstimates traffic and storage, separates reads from writes, adds a cache with a reason, and compares two code schemes.
SeniorLeads with the read/write ratio, chooses 302 because of click counts, takes counting off the hot path, explains what fails first at 10x, and names what they would leave out and why.
Defending the choiceStrong answer
Interviewer

Why not just hash the URL?

You

I could, but 7 characters of a hash collide, so I would still need a check-and-retry on every insert, and two users would share a code. Counter blocks give me no collisions with one allocator call per thousand links. The cost is guessable codes; if that matters here, I would scramble the number before encoding.

Follow-up questions

How would you run this in several regions?

Serve redirects from every region with a local cache and a local replica of the store; replication lag only delays brand-new links by a second or two. Give each region its own ID ranges so creates never coordinate across regions.

The same long URL is submitted twice. Same code?

Not by default. Deduplication needs an index on the long URL and lets one user's expiry or deletion affect another's link. Offer it per user if asked.

How do you stop abuse?

Rate limit creates per API key and per IP, check new URLs against a blocklist, and scan them asynchronously so a bad link can be disabled after the fact.

Mark it deleted in the store and delete the cache key. Because redirects use 302, browsers have not cached it, so the next click sees the change.

That is about 17,000 a second on one key. Keep a copy in each redirect server's memory for a few seconds so the cache node holding it is not overloaded.

Self-review: URL shortener0 of 13

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure