Handbooks / System Design Case Studies / Chapter 1
Design a URL Shortener
12 pages · ~18 min✓ Reviewed
Start here. Next up: Design a Rate Limiter.
Problem Statement, Scope and Requirements (URL Shortener)
1. Scope and Functional Needs
- In scope: create short link, redirect, custom alias, expiry, click analytics
- Out of scope: user auth UI
- Out of scope: link previews
- Out of scope: spam ML
| Operation | Input | Output |
|---|---|---|
| Create | POST long URL, alias optional, TTL optional | short URL |
| Redirect | GET short code | redirect to the long URL |
- Default TTL when none is given: 5 years
2. Non-Functional Targets
- Redirect latency: p99 under 50 ms
- Read availability: 99.99% on redirects
- Immutability: a created link never changes target
- Reads matter more than writes: a slow or failing redirect breaks every link ever shared
3. Capacity Estimates
High-Level Architecture (URL Shortener)
1. Components
| Component | Job | Scales by |
|---|---|---|
| Load balancer | Spreads requests, health-checks nodes | Add LB nodes |
| Stateless API servers | Validate, encode, look up, redirect | Add nodes |
| ID generator | Unique integer per new URL | Counter ranges or KGS |
| Cache (Redis) |
Short Code Generation: Base62 and Length (URL Shortener)
1. Alphabet and Length
- Base62 alphabet: 0-9, a-z, A-Z = 62 symbols per character
- Avoid base64's
+and/: URL-unsafe, they need escaping - Codes must be unique, short, and non-guessable if links are private
| Length | Formula | Codes | Verdict |
|---|
Code Strategies: Counter vs Hash vs Random (URL Shortener)
1. Hash and Counter
- Hash: MD5 or SHA-256 of the long URL, keep the first 7 base62 chars
- Stateless: any API server computes the same code for the same URL
- Collisions grow past ~1M links (birthday bound on 7 chars)
- Same long URL gives the same code, so it is dedup-friendly
- On insert, check the DB for the code
- On conflict, append a salt or counter to the URL and rehash
- Cost: one extra read per write
Redirect Service: 301 vs 302 (URL Shortener)
1. GET /{code} Flow
Custom Aliases and Link Expiry (URL Shortener)
1. Expiry Flow
- Lazy expiry: check
expires_aton every read, return 410 - No timer per link; an expired row costs nothing until it is read
- Daily batch job deletes expired rows and recycles the keys
- Batch is cleanup only; correctness comes from the read check
2. Data Model
Click Analytics Pipeline (URL Shortener)
1. Off the Redirect Path
- Never write analytics on the redirect path
- Redirect returns first, then publishes a click event to Kafka
- Event:
{code, ts, ip, user_agent, referrer} - Publish is asynchronous: the redirect never waits for Kafka
- Redirect302 sent
- Kafkaclick event
- Consumersaggregate
- ClickHousecolumn store
Read-Heavy Caching Layer (URL Shortener)
1. Cache-Aside Read Path
- Key is the short code, value is the
long_url - On miss: read the DB, then SET with a TTL
- Writes also populate the cache: a new link is hot at once
- Target: 95%+ hit ratio, miss path under 10 ms DB read
GET abc1234 SET abc1234 "https://example.com/long" EX 3600
hit returns the URL; on miss, fill with an EX TTL
Database Design and Sharding (URL Shortener)
1. Access Pattern
- Only query: lookup by
short_codereturnslong_url - No joins, no range scans, no secondary reads on the hot path
- NoSQL (DynamoDB, Cassandra) and sharded SQL both fit
- Writes: about 400/s at 10x peak; reads: about 40,000 QPS peak
2. Sharding and Sizing
- Shard key = hash(short_code) placed on a consistent hashing ring
- Hashing spreads load evenly: no range hot spots from sequential IDs
Scaling, Failures, Trade-offs and Interview Questions (URL Shortener)
1. Scale Path, Regions and Abuse
- API autoscalestateless, ~10 nodes
- Redis cluster3 shards, ~35 GB
- Read replicasasync, lag under 1 s
- CDNhot 301/302 targets
- Shard last~6 shards, ~50% fill
- Peak 40,000 QPS / 4,000 QPS per node = about 10 API nodes
- Stateless nodes: add or remove freely, no sticky sessions
- Sharding is last: ~3 TB in 5 years needs ~6 shards at ~50% fill (3 is the bare minimum)
URL Shortener Cheatsheet
Revision notes for the bit.ly-style URL Shortener: numbers, status codes, code generation, cache, analytics.
1. Estimates
| Metric | Estimate |
|---|---|
| Writes | 40/s avg, 400/s at 10x peak |
| Reads (100:1) | 4,000 QPS avg, 40,000 peak |
| Storage | 500 B x 100M x 12 x 5 yr = 3 TB |
Check yourself
A counter hands out ID 3844 (that is 62 squared). With the alphabet 0-9, a-z, A-Z, what short code does base62 encoding produce? What does the next ID tell you about this scheme?
- 3844 % 62 = 0, then 62 % 62 = 0, then 1 % 62 = 1; reversed, the remainders give
100 - Index 0 is '0' and index 1 is '1', so the code is
100, which is 1x62^2 - The next ID, 3845, encodes to
101, so neighbouring codes are neighbouring links - Sequential base62 leaks creation order and volume, and anyone can guess other codes
- If links are private, use a key service or random-looking codes instead