Design URL Shortener
bit.ly / TinyURL — hash or counter IDs, redirects, analytics, and collision handling at billions of clicks.
Interview tip Clarify read vs write ratio first (typically 100:1). Redirect path must be <100ms — cache hot URLs. Mention base62 encoding and 302 vs 301.
① Functional requirements
- Given a long URL, return a unique short code (e.g.
abc12X) - Redirect
GET /{code}→ original long URL (HTTP 302 or 301) - Optional custom alias if not taken (e.g.
/go/sale) - Optional expiration date per short link
- Click analytics: count + timestamp + referrer (async, not on critical path)
- Authenticated users can list and delete their links
Out of scope (state in interview)
- User authentication / OAuth (mention API keys only)
- Malware scanning of destination URLs
- Full-text search across all shortened URLs
- Editing long URL after creation
② Non-functional requirements
- Redirect latency p99 < 100ms globally
- 99.99% availability on read path
- Short codes must be globally unique
- Durable storage — no lost mappings
- Horizontally scalable writes (~40 URLs/sec avg, spikes 10×)
③ Back-of-the-envelope scale
Assumptions
- 100M new URLs/month → ~40 writes/sec avg, ~400/sec peak
- Read:write ratio 10:1 → ~400 reads/sec avg, ~4K/sec peak
- Avg long URL ~2 KB metadata; 5-year retention → ~6B URLs
- Storage: 6B × (8B code + 2KB URL + metadata) ≈ 12 TB+
- Redirect bandwidth: 4K RPS × 500B response ≈ 2 MB/s (tiny — CPU/cache bound)
- Analytics: 4K clicks/sec → ~350M events/day → Kafka → columnar store
The read path dominates cost and engineering focus. Cache the top 20% of codes (Pareto) in Redis + CDN edge to serve ~80% of redirects without hitting origin DB.
④ High-level architecture
URL Shortener — read/write split
Clients / Browsers
Mobile Apps
CDN / Edge Cache (redirects)
Load Balancer
API Servers (create, manage)
Redirect Servers (lookup)
Redis Cache (hot codes)
Primary DB (code ↔ URL)
ID Generator (Snowflake / counter)
Kafka → Click Analytics DB
POST /shorten→Generate ID→Persist mapping→Return short URL
Separate redirect fleet from API fleet — redirects are cache-friendly and need different autoscaling. Use 302 for flexibility (change destination); 301 only if permanent and SEO matters.
⑤ Data flow & execution path
End-to-end execution flow
① Client→② API / LB→③ Core services→④ Cache + DB→⑤ Message queue→⑥ Async workers
Sync path: validate → authorize → read/write primary store
Async path: publish domain events → consumers (email, analytics, search index)
Read-heavy path: CDN / edge cache → regional cache → DB replica
Failure path: retry with backoff, DLQ, idempotent handlers
In interviews, trace one user action through this diagram. State what is synchronous (user waits) vs asynchronous (background), and where you enforce idempotency.
⑥ API & interfaces
| Endpoint / flow | Purpose | Notes |
|---|---|---|
| POST /api/v1/urls | Create short URL | Body: {longUrl, customAlias?, expiresAt?} → 201 {shortUrl, code} |
| GET /{code} | Redirect | 302 to long URL; 404 if expired/missing; cache headers for CDN |
| GET /api/v1/urls/{code}/stats | Analytics | Auth required; returns click count, time series |
| DELETE /api/v1/urls/{code} | Delete mapping | Soft-delete + cache invalidation |
| GET /api/v1/urls?userId= | List user links | Paginated; cursor-based |
⑦ Data model & storage
urls table:
clicks (analytics):
id (PK bigint), short_code (unique index, 7 chars base62), long_url (varchar 2048), user_id, created_at, expires_at, is_activeclicks (analytics):
code, timestamp, referrer, country — partitioned by day in ClickHouse/BigQuery| Store | What | Why |
|---|---|---|
| PostgreSQL / Cassandra | URL mappings | Strong uniqueness on short_code; Cassandra if write scale exceeds single-shard SQL |
| Redis Cluster | Hot redirect cache | TTL 24h; cache-aside on miss; pub/sub invalidation on delete |
| S3 + CDN | Static redirect responses (optional) | Edge workers for ultra-low-latency redirects at scale |
| Kafka + ClickHouse | Click stream | Append-only; batch ingest; rollups for dashboards |
⑧ Deep dive — core components
ID generation — counter vs hash
Counter + Base62: Distributed ID service (Snowflake-style: timestamp + machine ID + sequence) guarantees uniqueness without DB round-trip for collision check. 7 base62 chars = 62⁷ ≈ 3.5 trillion codes — plenty for 6B URLs. Pros: no collisions, sortable by time. Cons: predictable (security through obscurity weak — use random suffix if needed).
Hash (MD5/SHA truncated): Hash long URL + salt → first 7 chars. Pros: same long URL → same short URL (dedup). Cons: collisions require probing or lengthening code; not sequential. Use bloom filter + DB check on write.
Redirect hot path optimization
On
GET /{code}: (1) Check CDN edge — if cached 302, return immediately. (2) Check local Redis — sub-ms. (3) DB lookup on miss; populate Redis + CDN. Set Cache-Control: max-age=3600 for popular links. Viral link mitigation: singleflight pattern — only one DB query per cache miss storm.Geo-routing: anycast DNS to nearest redirect POP. For global 100ms p99, edge compute (Cloudflare Workers) can hold top-10K codes in KV store updated via pub/sub from origin.
Analytics pipeline (off critical path)
Redirect server fires async event to Kafka:
{code, ts, ip_hash, referrer, ua}. No synchronous write to analytics DB on redirect — would kill latency. Consumers aggregate into hourly rollups in ClickHouse. Display "approximate" counts with ±1% freshness lag acceptable for dashboards.Privacy: hash IPs, truncate referrer, GDPR delete propagates to analytics store via compaction jobs.
Custom alias & expiration
Custom alias: check uniqueness in DB with unique index; reserve abusive words via blocklist. Expiration: lazy delete on read (if expired → 410 Gone) + nightly cron to purge + invalidate cache. TTL index in Redis mirrors DB expiration for hot codes.
⑨ Trade-offs & alternatives
| Decision | Option A | Option B | Pick when |
|---|---|---|---|
| ID strategy | Monotonic counter | Hash of URL | Counter for scale; hash if dedup matters |
| Redirect code | 302 Found | 301 Moved Permanently | 302 default; 301 only for permanent marketing links |
| Database | PostgreSQL | Cassandra | PG until ~10K writes/sec; Cassandra for global multi-DC |
| Cache invalidation | TTL only | Delete on write | TTL for simplicity; delete-on-write for correctness |
| Analytics | Real-time stream | Batch hourly | Stream for dashboards; batch cheaper at huge scale |
⑩ 45-minute interview script
- 0–5 min: Clarify functional + non-functional requirements; confirm analytics and custom aliases
- 5–10 min: Back-of-envelope — writes/sec, reads/sec, storage for 5 years
- 10–20 min: High-level diagram — API, redirect, DB, cache, CDN; separate read/write paths
- 20–30 min: Deep dive ID generation and redirect hot path with latency budget
- 30–38 min: Data model, API contracts, analytics async pipeline
- 38–42 min: Trade-offs — 302 vs 301, SQL vs NoSQL, cache strategy
- 42–45 min: Failure modes — DB down (serve from cache), hot key, ID collision
⑪ Likely follow-up questions
| Question | Short answer |
|---|---|
| What if two users want the same custom alias? | First-write-wins; DB unique constraint returns 409 Conflict |
| How handle a viral link hitting one Redis shard? | Local in-process cache + CDN; replicate hot key to all nodes; singleflight on miss |
| 301 vs 302? | 302 allows changing destination and avoids SEO credit transfer; 301 caches aggressively at browsers |
| How to prevent abuse (spam URLs)? | Rate limit per IP/API key; blocklist domains; optional manual review for custom aliases |
| Multi-region deployment? | Cassandra or CockroachDB multi-DC; Redis per region with async replication; CDN handles most reads |
| How to migrate from 6-char to 7-char codes? | Dual-read: try 6 then 7; new codes get 7; no migration needed for old |
⑫ Revision checklist
- Stated read:write ratio and why read path is optimized
- Calculated writes/sec and storage (TB scale)
- Chose ID strategy with collision handling
- Drew separate redirect vs API path
- Redis + CDN on redirect hot path
- Analytics async (not blocking redirect)
- API: POST shorten, GET redirect, optional stats
- Discussed 302 vs 301 trade-off
- Custom alias uniqueness constraint
- Failure mode: DB down, cache stampede