URL Shortener
The six-step method applied end to end — ID generation, a 100:1 read ratio, and why the redirect path decides the architecture
The classic first design problem. It looks trivial and it is not: the ID generation question alone has four reasonable answers with different failure modes.
1. Clarify and Scope
Questions worth asking before drawing anything:
| Question | Assumption taken here |
|---|---|
| Custom aliases? | No — system-generated codes only |
| Do links expire? | Yes, optional expiry |
| Analytics? | Click counts only, not real-time |
| How long are the codes? | As short as possible |
| Can a link be deleted? | Yes |
In scope: shorten a URL, redirect, count clicks. Out of scope: user accounts, custom domains, editing a link's target.
2. Put Numbers On It
WRITES
100 million new links per month
100M ÷ (30 × 100,000 sec) ≈ 33 writes/sec
× 3 peak ≈ 100 writes/sec ← tiny
READS
Assume each link is clicked 100 times
10 billion redirects per month
10B ÷ (30 × 100,000) ≈ 3,300 reads/sec
× 3 peak ≈ 10,000 reads/sec
Read : write = 100 : 1 ← the key finding
STORAGE
short code 7 bytes
long URL 100 bytes
metadata 100 bytes (created, expiry, owner, clicks)
per row ≈ 210 bytes, round to 500 with indexes
100M/month × 500 bytes × 12 × 5 years ≈ 3 TBWhat these four numbers decided
| Finding | Consequence |
|---|---|
| Only 100 writes/sec | One database primary is plenty. No write sharding |
| 10,000 reads/sec | Cache hard. This is the path to optimise |
| 100:1 read ratio | Every design decision should favour reads |
| 3 TB over five years | Fits comfortably on one machine. No sharding needed |
The whole system is read-heavy and small. That rules out most of the complexity people reach for.
How long must the code be?
Alphabet: a-z A-Z 0-9 = 62 characters
62^6 = 56 billion
62^7 = 3.5 trillion ← 7 characters is ampleAt 100 million links per month, seven characters lasts thousands of years.
3. Define the Interface
POST /urls
body: { "url": "https://example.com/very/long/path", "expiry": "2027-01-01" }
returns: { "code": "aB3xK9p", "short": "https://sho.rt/aB3xK9p" }
GET /{code}
returns: 301 or 302 redirect to the original URL
GET /urls/{code}/stats
returns: { "code": "aB3xK9p", "clicks": 1423 }301 or 302 — this choice has consequences
301 Permanent — browsers cache it. Later clicks never reach your servers at all, which massively cuts load. But you can no longer count clicks, and you can never change where the link points.
302 Found — every click reaches you. You get analytics and can change the target, at the cost of serving all that traffic.
Since this design counts clicks, 302 is the right call. Say it out loud: "302, because we need click counts — a 301 would be cheaper but the browser cache would hide the traffic from us."
4. Generating the Code
This is the real design question, and there are four answers.
| Approach | How | Problem |
|---|---|---|
| Hash the URL | base62(md5(url))[:7] | Collisions must be detected and retried |
| Random | 7 random base62 characters | Collisions; needs a uniqueness check |
| Auto-increment + base62 | Encode a counter | Codes are guessable and leak volume |
| Counter range per server | Each server claims a block of ids | Best balance — no coordination per write |
Counter ranges are the strongest answer
A central service hands each application server a block of one million ids. The server encodes them to base62 locally with no coordination per write.
- No collisions ever — ranges do not overlap.
- No database check on the write path.
- A server crash wastes at most one unused block, which is harmless.
The remaining flaw is that sequential ids are guessable and reveal how many links exist. If that matters, multiply the counter by a large odd number modulo 62⁷, which scrambles the order while staying collision-free.
Server A holds 1,000,000 – 1,999,999
Server B holds 2,000,000 – 2,999,999
id 1,000,001 → base62 → "4c92"Why hashing alone is not enough
md5(url) truncated to 7 characters will collide — by the birthday paradox, expect the first collision after roughly √(62⁷) ≈ 1.8 million links, which you reach in under a month.
You then need a database read on every write to check the code is free, plus a retry loop. That turns a coordination-free write into a coordinated one.
5. The Architecture
The database
urls
code VARCHAR(7) PRIMARY KEY
long_url TEXT NOT NULL
created_at TIMESTAMP
expires_at TIMESTAMP NULL
click_count BIGINT DEFAULT 0code as the primary key means the redirect is a single primary-key lookup — the fastest read a database offers. No secondary index is needed on the hot path.
Never increment click_count on the redirect
UPDATE urls SET click_count = click_count + 1 WHERE code = ?That is a write, with a row lock, on every single redirect. It turns a 100 writes/sec system into a 10,000 writes/sec system, and hot links serialise on one row.
Push click events onto a queue and aggregate them in batches. Counts become a few seconds stale, which the scoping step already accepted.
6. What Was Traded Away
| Decision | Gained | Cost |
|---|---|---|
| 302 instead of 301 | Click counts, changeable targets | Every click hits our servers |
| Cache in front | ~90% of reads never reach the DB | Deleted links may redirect for up to the TTL |
| Async click counting | Write load stays at 100/sec | Counts lag by seconds |
| Counter ranges | No collision checks, no coordination | Codes are sequential unless scrambled |
| Single primary | Simple, no sharding | One machine's write capacity — ample here |
Follow-on Questions
Related Reading
- Numbers Every Design Needs — the estimation used here
- Caching — the layer doing the real work
- Rate Limiter — protecting the write path
- Databases — indexing and when sharding is justified
Deployment and Migrations
Shipping changes without downtime — rolling, blue-green and canary releases, feature flags, and the expand-contract pattern for schema changes
Rate Limiter
Four algorithms compared with their exact failure modes, and the distributed counting problem that makes this harder than it looks