The problem
Design a URL shortener like bit.ly. Users paste a long URL and get a short link back; anyone who opens the short link is redirected to the original URL.
Your company embeds links in SMS campaigns, where every character counts. Marketing wants branded short links with click analytics.
If redirects are slow or links break, every campaign in flight silently loses traffic — and customers notice within minutes.
Constraint 1
Redirects must feel instant (p99 under 100 ms)
Constraint 2
Short links must keep working for years
Constraint 3
Read traffic vastly outweighs writes
Step 1 · What to build (requirements)
A strong candidate never designs from the one-line brief — they pin down what the system must do (functional requirements) and how well it must do it (non-functional: speed, scale, durability) before drawing anything.
Here is the requirement set this design is built on. Notice how each one is a single, checkable sentence — and how the non-functional ones are measurable, not vibes.
The requirements
| Type | Requirement | Priority |
|---|---|---|
| Functional | Create a short link for a given long URL | Must have |
| Functional | Redirect a short link to its original URL | Must have |
| Functional | Support custom (vanity) aliases | Nice to have |
| Functional | Track click counts per link | Nice to have |
| Non-functional | Low-latency redirects (p99 < 100 ms) | Must have |
| Non-functional | Highly available redirect path (reads keep working during failures) | Must have |
| Non-functional | Links are durable for years (never lose a mapping) | Must have |
| Non-functional | Handle a read-heavy workload (~100:1 read/write ratio) | Nice to have |
What you learn by asking
In an interview these facts are NOT volunteered — the candidate earns them with clarifying questions. These are the answers behind this design.
| If you ask about… | The answer |
|---|---|
| traffic volume | Expect around 10 million new links per month and roughly 100 redirects per link over its lifetime. |
| link expiry | Links do not expire by default; paid customers can set an optional TTL. |
| analytics depth | Click counts and rough geography are enough — no per-user tracking. |
| short code length | Codes should stay at 7 characters or fewer. |
Step 2 · The numbers (back-of-envelope)
Rough arithmetic turns the requirements into engineering decisions: it tells you which parts of the design need scaling machinery and which are trivially easy. Only the order of magnitude matters.
The worked numbers
| Quantity | Answer | How it is derived |
|---|---|---|
| Average write QPS (new links per second) | ~4 writes/sec | 10M links/month ÷ (30 days × 86,400 s) ≈ 10,000,000 ÷ 2,592,000 ≈ 4 writes/sec. |
| Average redirect QPS | ~400 reads/sec | With a ~100:1 read/write ratio, 4 writes/sec × 100 ≈ 400 redirects/sec on average. |
| Storage needed for 5 years of links | ~300 GB | 10M links/month × 60 months = 600M links; 600M × 500 bytes = 300 GB. |
Step 3 · The API
Every operation traces back to a requirement — nothing extra, nothing missing.
| Endpoint | Purpose |
|---|---|
| POST /v1/links | Accepts the long URL (and optional custom alias), returns the short code. |
| GET /{code} | Looks up the code and returns a 301/302 redirect to the long URL. |
| GET /v1/links/{code}/stats | Returns click counts for a link the caller owns. |
The data model
| Entity | Fields |
|---|---|
| Link | code: string (primary key, ≤7 chars), longUrl: string, ownerId: string, createdAt: timestamp, expiresAt: timestamp? (optional TTL) |
| ClickEvent | code: string, at: timestamp, country: string? |
Step 4 · The architecture, one version at a time
Nobody designs the final diagram in one stroke. Strong candidates start with the simplest thing that works, then let the numbers break it: each version fixes exactly one bottleneck with exactly one new component.
You can stop reading after any version and still hold a complete, working system in your head — that is the point.
v1 — the simplest thing that works
The fix: One app server and one database table. The table has two columns — the short code and the long URL it stands for.
Creating a link inserts one row: code x7Ab9Q2 next to the full URL. Opening a link reads that row back and answers with a redirect — a tiny HTTP response telling the browser "the page you want is over there".
This is a complete URL shortener. It creates links, it redirects, and for a small workload nothing more is needed. Every version that follows exists only because a specific number breaks this one.
v1 — the simplest thing that works — the picture
Why each connection exists
- ClientApp ServiceBrowsers call the app directly — no other machinery exists yet.
- App ServiceDatabaseEvery lookup and every new link is one row in the table.
v2 — reads outnumber writes 100 to 1
Where it breaks: The numbers say ~4 new links/sec but ~400 redirects/sec, and every redirect must finish in under 100 ms. In v1 each of those 400 reads is a database query — the database becomes the bottleneck long before the write path feels anything.
The fix: Add a cache — a small, very fast in-memory store that keeps the answers we serve most often, so popular links never touch the database at all.
On a redirect the app asks the cache first. A hit answers in well under a millisecond; only a miss falls through to the database, and the answer is stored in the cache on the way back so the next visitor hits.
Because a few popular links attract most clicks, even a small cache absorbs the bulk of those 400 reads/sec. The database is back to a workload it handles easily.
v2 — reads outnumber writes 100 to 1 — the picture
Why each connection exists
- App ServiceCacheHot lookups hit the cache first to keep redirects fast.
- App ServiceDatabaseCache misses and new links go to the durable store.
v3 — one app server is now the weak point
Where it breaks: Redirects must keep working through failures, but v2 still has a single app server: if it restarts or dies, every campaign link in the world is dead until it comes back. One machine is also a hard ceiling on how many redirects we can serve at once.
The fix: Run several identical app servers behind a load balancer — a traffic director that spreads incoming requests across the servers and stops sending to any server that fails a health check.
The app servers keep no local state — every mapping lives in the cache and the database — so any server can answer any request. Losing one just shifts its share of traffic to the others.
The one new wrinkle is code generation: two servers must never mint the same short code. Giving each server a pre-allocated block of IDs to encode keeps creates collision-free without the servers coordinating on every write.
v3 — one app server is now the weak point — the picture
Why each connection exists
- ClientLoad BalancerAll traffic enters through the load balancer.
- Load BalancerApp ServiceThe balancer fans requests out to app servers.
- App ServiceCacheHot lookups hit the cache first to keep redirects fast.
- App ServiceDatabaseCache misses and new links go to the durable store.
v4 — counting clicks without slowing redirects
Where it breaks: Marketing wants click counts, but writing a click row during the redirect adds a database write to every one of those 400 reads/sec — the hot path we just spent two versions protecting.
The fix: Push each click onto a queue — a buffer that accepts an event instantly and lets a separate worker read and count it later, so the redirect never waits on analytics.
The redirect handler drops a tiny "code X was clicked" event onto the queue and answers immediately. A background consumer drains the queue at its own pace and updates the counts.
If analytics falls behind or breaks, redirects are untouched — the events just wait in the buffer. That one-way decoupling is why the should-have feature costs the must-have path nothing.
The finished design
The full reference design, with the reason behind every connection. When you practice, you will rebuild this from a palette that includes decoys.
Why each connection exists
- ClientLoad BalancerAll traffic enters through the load balancer.
- Load BalancerApp ServiceThe balancer fans requests out to app servers.
- App ServiceCacheHot lookups hit the cache first to keep redirects fast.
- App ServiceDatabaseCache misses and new links go to the durable store.
- App ServiceAnalytics Queue(optional)Clicks are queued asynchronously so redirects never wait on analytics.
Step 5 · Defending it (deep dives)
Interviewers close by stress-testing the design. Strong answers follow one shape: which component → what breaks there → the fix and its cost. Here are this problem's probes and the points a strong answer hits:
One link goes viral and receives 50,000 redirects per second. What breaks first, and how do you handle it? A single hot key overwhelms one cache shard/partition. Replicate or locally cache the hot entry (e.g. in-process cache, CDN). The database is protected as long as the cache absorbs the reads.
Your cache cluster goes down entirely. What happens to redirects, and how do you keep the site up? All reads fall through to the database — a thundering herd. Protect the DB with rate limiting / request coalescing. Serve from replicas; warm the cache gradually on recovery.
How do you generate short codes so two simultaneous requests never produce the same code? Counter/sequence with base62 encoding, or random codes with uniqueness check. Pre-allocated ranges per app server avoid a central bottleneck. Hashing the URL alone collides and leaks; needs a disambiguator.
Marketing expands to three continents. How does the design change to keep redirects fast everywhere? Replicate read path (cache + DB replicas) per region. Writes can stay single-region or use async replication — links tolerate seconds of propagation. Route users to the nearest region (GeoDNS/anycast).
The design in one glance
Each version fixed exactly one problem. If you can retell this table from memory, you can derive the whole architecture on a whiteboard.
| Version | What broke | The fix |
|---|---|---|
| v1 — the simplest thing that works | — the starting point | One app server and one database table. The table has two columns — the short code and the long URL it stands for. |
| v2 — reads outnumber writes 100 to 1 | The numbers say ~4 new links/sec but ~400 redirects/sec, and every redirect must finish in under 100 ms. In v1 each of those 400 reads is a database query — the database becomes the bottleneck long before the write path feels anything. | Add a cache — a small, very fast in-memory store that keeps the answers we serve most often, so popular links never touch the database at all. |
| v3 — one app server is now the weak point | Redirects must keep working through failures, but v2 still has a single app server: if it restarts or dies, every campaign link in the world is dead until it comes back. One machine is also a hard ceiling on how many redirects we can serve at once. | Run several identical app servers behind a load balancer — a traffic director that spreads incoming requests across the servers and stops sending to any server that fails a health check. |
| v4 — counting clicks without slowing redirects | Marketing wants click counts, but writing a click row during the redirect adds a database write to every one of those 400 reads/sec — the hot path we just spent two versions protecting. | Push each click onto a queue — a buffer that accepts an event instantly and lets a separate worker read and count it later, so the redirect never waits on analytics. |
Tip — Key takeaway
Separate the write path (rare, can be slower) from the read path (hot, cached, replicated) — the whole design falls out of that asymmetry.
Common trap — Common mistakes on this problem
Treating reads and writes as equally frequent · Coupling analytics to the redirect hot path · Ignoring durability — losing mappings breaks links forever
You've seen the whole design — now build it yourself
The practice run walks the same five steps, but YOU do the work: gather the requirements, run the numbers, wire the architecture — with an AI interviewer and per-step feedback.