The 100 problems, solved / Storage and files / 011 · Design a URL shortener

Design a URL shortener

Billions of short links, custom aliases, 100:1 read to write, and nothing ever collides.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Functional

  • Given a long URL, return a short one (about 7 characters); visiting the short URL redirects to the long one.
  • Optional custom alias chosen by the user; optional expiry; the creator can see click counts.
  • Out of scope: user accounts beyond an API key, link previews, editing a link's target after creation.

Non-functional

  • 100 million new links a month, 10 billion redirects a month (100:1 read to write); links kept for 5 years.
  • Redirect latency under 50 ms at p99 worldwide; availability 99.99 percent for redirects.
  • Short codes never collide and are not guessable in sequence (so a crawler cannot enumerate private links).
  • Abuse: malicious targets are blocked; creation is rate limited per key.
Back of the envelope

Writes: 108 / (30 × 86,400) ≈ 40 links/s; reads: 1010 / (30 × 86,400) ≈ 3,900 redirects/s average, say 20,000/s at peak.

Storage: 100 million/month × 60 months = 6 × 109 links; at 500 bytes each (long URL, code, timestamps, owner) that is 3 TB. Small enough for a few shards; the challenge is read latency and availability, not size.

Code space: 62 characters (a–z, A–Z, 0–9) to the power 7 is 3.5 × 1012, about 600 times the 5-year volume, so 7 characters is enough and random codes rarely collide.

2. Core entities

  • Link: code (primary key), long URL, owner key, created-at, expires-at, status (active, blocked), custom flag.
  • Click: code, timestamp, country, referrer (aggregated, not stored per click at scale).
  • API key: key id, owner, rate-limit tier.

3. API

  • POST /links body: long_url, optional alias, optional expires_at → 201 with code and short URL; 409 if the alias is taken.
  • GET /{code} → 302 (or 301) redirect to the long URL; 404 or 410 when missing or expired.
  • GET /links/{code}/stats → clicks by day and country.
  • DELETE /links/{code} → owner only.

4. High-level design

ClientsEdgeServicesDataGET /codemissPOST /linkson cache missclick eventnext idinsertBrowserAPI clientEdge / CDN(redirect cache)Load balancer+ rate limiterRedirect serviceCreate serviceAnalytics consumerCache (code → URL)Link DB(sharded by code)Click streamID generator

Creating and following a link:

  1. The create service validates the long URL, checks it against the block list, obtains a unique id from the ID generator and encodes it in base 62 (or uses the custom alias), and inserts the row. The code is returned.
  2. A visitor hits GET /abc12Xy. The edge checks its cache; on a miss the redirect service checks the shared cache, then the database shard for that code, and returns a 302.
  3. The redirect service drops a click event onto a queue and returns without waiting; a consumer aggregates clicks per code per hour into the stats table.

5. Deep dives

How are codes generated so they never collide and cannot be enumerated?

OptionCollisionsGuessableCoordination
Hash the long URL, take 7 charsyes, must check and retry; same URL gives same code (sometimes wanted)nonone
Auto-increment counter, base 62neveryes, sequentiala single counter is a bottleneck
Ranges from a counter service, encoded with a secret permutationnevernoone call per 10,000 codes
Random 7-char code, insert-if-absentrare (3.5 × 1012 space); retry on conflictnonone

Choice: each create server leases a range of 10,000 ids from a counter service (a replicated store doing one atomic increment per lease), and the id is passed through a keyed permutation before base-62 encoding so consecutive ids produce unrelated codes. No collision check is needed, there is no per-request coordination, and a server that dies wastes at most its unused range. Random codes with insert-if-absent are the other good answer and simpler; say what you would pick and why. Custom aliases go through a uniqueness check on the alias shard and are never allowed to look like generated codes.

Redirects at 20,000 a second under 50 ms worldwide

The data is 3 TB and the hot set is tiny: a small fraction of links gets most clicks. Three layers: the edge caches the redirect response per code for a short TTL (minutes, so a blocked link stops quickly); a shared in-memory cache in each region holds code → URL for tens of millions of hot codes (a few GB); the database serves misses and is sharded by code with read replicas in every region. Reads are keyed by exactly the primary key, so the database path is a single point lookup. The choice between 301 and 302 matters: a 301 lets browsers cache the redirect forever, which saves traffic but stops you counting clicks and blocking a link that turns malicious later; use 302 unless the customer asks otherwise.

What if a shard or a region goes down?

Redirects are reads of immutable data, which is the easy case for availability: every shard has replicas in at least two regions, and regional caches keep serving hot links even while a database is unreachable. Creation is the write path and may degrade: with the counter service unreachable, servers keep issuing from their leased ranges for a while, and if ranges run out, creation fails while redirects continue, which is the right failure order. Say that explicitly; interviewers like a design that fails in a chosen direction.

Abuse and malicious links

Creation is rate limited per API key and per IP. Each new target is checked against a safe-browsing list synchronously and rescanned asynchronously, because a target can turn bad after creation; a hit flips the link to blocked, the edge cache entry is purged, and the redirect becomes a warning page. Open redirect abuse (attackers hiding phishing behind a trusted domain) is the reason the warning interstitial exists for links flagged by reputation signals such as a sudden spike in clicks from many countries.

Click analytics without slowing the redirect

Counting on the redirect path (an increment in the database per click) turns a 20,000 reads/s system into a 20,000 writes/s system on a few hot rows. Instead the redirect service writes a small event (code, timestamp, country from the edge, hashed referrer) to a queue and forgets it. A consumer aggregates events per code per hour and writes one row per bucket, so the stats table receives thousands of rows an hour rather than millions. Exactly-once is not needed for click counts, so at-least-once delivery with a tolerance for the rare duplicate keeps the pipeline simple; the owner's dashboard reads the hourly buckets and sums them. If per-click detail is a product requirement, store the raw events in object storage partitioned by day and query them offline.

6. What a strong answer sounds like

  • Show the ratio: 40 writes/s against 20,000 reads/s, so the design is about the read path and availability, not storage.
  • Choose an id scheme and defend it on collisions, guessability and coordination; the ranged counter with a permutation is a crisp answer.
  • Lay out the three cache layers and the 301 versus 302 trade-off.
  • State the failure order: redirects keep working when creation cannot.
  • Expect follow-ups on analytics at scale (the click stream and hourly aggregation), on expiry (lazy on read plus a sweeper), and on a custom-domain feature.
  • Common mistake: hashing the URL and ignoring collisions, or a single global auto-increment counter on the write path.

02Where this question was reported