System designIntermediate
Design a URL Shortener
A service that turns a long URL into a 7-character code and redirects anyone who opens it. Small feature, complete system: it forces the ID-generation decision, a read-heavy cache, the 301-vs-302 analytics tradeoff and a data set that outgrows one database.
Functional requirements
- Given a long URL, return a short code (
https://sho.rt/aZ3kQ9x); optionally a custom alias and an expiry date. - Opening the short URL redirects to the original URL.
- A code must be unique and must not be guessable from the previous one (no enumerating other people’s links).
- Authenticated users can list, inspect and delete their links.
- Click analytics per link: total clicks, clicks per day, top referrers and countries.
- Expired links return 410 Gone; unknown codes return 404.
- Abuse controls: rate limit link creation, block known-malicious destinations.
Non-functional requirements
Scale, latency, availability and durability targets — these decide the architecture.
- Redirect latency p99 < 50 ms at the service (the user-visible cost is the redirect itself, not our compute).
- Availability 99.99% for redirects (52 min/yr); 99.9% for link creation — reads matter more than writes here.
- Scale: 100M new links/day, read:write ratio 10:1, retained 5 years.
- Durability: a created link must never be lost — a short URL printed on a poster cannot be re-issued.
- Codes stay 7 characters for at least 10 years of growth.
Back-of-the-envelope
Numbers first. Every component below has to be justified by one of these.
| Quantity | Value | Arithmetic |
|---|---|---|
| Write rate | ≈ 1,160 /s avg, 3,500 /s peak | 100M new URLs/day ÷ 86,400 s ≈ 1,160 writes/s; peak 3× → ~3,500/s. Any single Postgres handles this. |
| Read rate | ≈ 11,600 /s avg, 35k /s peak | 10:1 read ratio → 11,600 redirects/s; peak 3× → ~35,000/s. A single DB doing 5–10k point reads/s per replica is not enough on its own — this number is why the cache exists. |
| Storage | ≈ 18 TB/yr, 91 TB in 5 years | 100M rows/day × ~500 B (code, URL up to 2 KB but ~300 B median, owner, timestamps) = 50 GB/day × 365 = 18 TB/yr. Past ~10 TB one Postgres is uncomfortable → shard or use a KV store. |
| Key space | 62^7 ≈ 3.5 × 10^12 | Base62 (a–z, A–Z, 0–9), 7 chars = 3.52 trillion codes. At 100M/day that is 35,000 days ≈ 96 years. 6 chars (5.7 × 10^10) would last only 1.5 years. |
| Cache size | ≈ 10 GB for the hot set | 1.16B reads/day, but ~20% of links take ~80% of reads. If 20M distinct codes are hot on a given day × ~500 B = 10 GB — one Redis node, with headroom on a 32 GB box. |
| Bandwidth | ≈ 6 MB/s out | 11,600 redirects/s × ~500 B response (headers + Location) ≈ 5.8 MB/s. Bandwidth is irrelevant; latency and DB reads are the constraints. |
Interface
Endpoints, messages or events.
POST /api/links { url, alias?, expires_at? } → 201 { code, short_url }Requires an API key or session. Not naturally idempotent: two identical calls create two codes unless the client sends an Idempotency-Key; we store key → response for 24 h. Validate the URL (scheme http/https, length ≤ 2 KB, not our own domain).GET /{code} → 302 Location: <url>The hot path. No auth, no body. 302/307 (not 301) so the browser comes back every time and we can count the click — see the 301-vs-302 decision. Unknown → 404, expired → 410.GET /api/links/{code} → { url, created_at, expires_at, clicks }Owner only. clicks is the eventually consistent counter from analytics, not a live DB count.GET /api/links?cursor=&limit=50 → { items, next_cursor }The owner’s links, newest first; cursor = (created_at, code), never an offset.DELETE /api/links/{code} → 204Idempotent: deleting twice returns 204. Must also evict url:{code} from Redis or the redirect keeps working for up to a TTL.GET /api/links/{code}/stats?from=&to= → { total, by_day[], referrers[], countries[] }Served from the analytics store (daily rollups), not from the links table.Build it one problem at a time
Each step names the problem first. Decide what you would add before revealing the reference answer.
1
Start: one service, one table
Problem · Two operations — create a code, resolve a code. The simplest thing that meets the requirement is one stateless service and one table keyed by
code. Resist drawing ten boxes before there is a measured reason.Work through every step to unlock the data model, the request walkthrough, scaling, failure modes and the open decisions.