Design a URL shortener
Billions of short links, custom aliases, 100:1 read to write, and nothing ever collides.
- Unique ID generation — IDs that are unique, roughly ordered and made without a central counter.
- Caching strategies — Read-through, write-through, write-back, TTLs and invalidation.
- Sharding and partitioning — Splitting data by key so each machine holds a slice it can handle.
- Consistent hashing — Assigning keys to machines so adding one machine moves only a few keys.
- Rate limiting — Token buckets, sliding windows, and enforcing them across many servers.
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.
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 /linksbody: 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
Creating and following a link:
- 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.
- 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. - 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?
| Option | Collisions | Guessable | Coordination |
|---|---|---|---|
| Hash the long URL, take 7 chars | yes, must check and retry; same URL gives same code (sometimes wanted) | no | none |
| Auto-increment counter, base 62 | never | yes, sequential | a single counter is a bottleneck |
| Ranges from a counter service, encoded with a secret permutation | never | no | one call per 10,000 codes |
| Random 7-char code, insert-if-absent | rare (3.5 × 1012 space); retry on conflict | no | none |
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
- Exponent · 'How would you build a URL shortener along the lines of TinyURL?'
- Exponent · (see rate limiter) same experience page, L5
- interviewkickstart.com · Listed
- interviewkickstart.com · Core Q1, 'a standard question'
- interviewkickstart.com · Q1: 'globally distributed URL shortening service'
- DesignGurus · 'like goo.gl'; page notes it is not uniquely Google
- DesignGurus · Listed (Bit.ly)
- DesignGurus · Asked at Google, Meta and Amazon alike
- DesignGurus · Listed
- DesignGurus · Listed
- Educative · 'Design TinyURL'
- IGotAnOffer · Google TPM list (Glassdoor-sourced)