System Design

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 /shortenGenerate IDPersist mappingReturn 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 / flowPurposeNotes
POST /api/v1/urlsCreate short URLBody: {longUrl, customAlias?, expiresAt?} → 201 {shortUrl, code}
GET /{code}Redirect302 to long URL; 404 if expired/missing; cache headers for CDN
GET /api/v1/urls/{code}/statsAnalyticsAuth required; returns click count, time series
DELETE /api/v1/urls/{code}Delete mappingSoft-delete + cache invalidation
GET /api/v1/urls?userId=List user linksPaginated; cursor-based

⑦ Data model & storage

urls table: id (PK bigint), short_code (unique index, 7 chars base62), long_url (varchar 2048), user_id, created_at, expires_at, is_active

clicks (analytics): code, timestamp, referrer, country — partitioned by day in ClickHouse/BigQuery
StoreWhatWhy
PostgreSQL / CassandraURL mappingsStrong uniqueness on short_code; Cassandra if write scale exceeds single-shard SQL
Redis ClusterHot redirect cacheTTL 24h; cache-aside on miss; pub/sub invalidation on delete
S3 + CDNStatic redirect responses (optional)Edge workers for ultra-low-latency redirects at scale
Kafka + ClickHouseClick streamAppend-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

DecisionOption AOption BPick when
ID strategyMonotonic counterHash of URLCounter for scale; hash if dedup matters
Redirect code302 Found301 Moved Permanently302 default; 301 only for permanent marketing links
DatabasePostgreSQLCassandraPG until ~10K writes/sec; Cassandra for global multi-DC
Cache invalidationTTL onlyDelete on writeTTL for simplicity; delete-on-write for correctness
AnalyticsReal-time streamBatch hourlyStream for dashboards; batch cheaper at huge scale

⑩ 45-minute interview script

  1. 0–5 min: Clarify functional + non-functional requirements; confirm analytics and custom aliases
  2. 5–10 min: Back-of-envelope — writes/sec, reads/sec, storage for 5 years
  3. 10–20 min: High-level diagram — API, redirect, DB, cache, CDN; separate read/write paths
  4. 20–30 min: Deep dive ID generation and redirect hot path with latency budget
  5. 30–38 min: Data model, API contracts, analytics async pipeline
  6. 38–42 min: Trade-offs — 302 vs 301, SQL vs NoSQL, cache strategy
  7. 42–45 min: Failure modes — DB down (serve from cache), hot key, ID collision

⑪ Likely follow-up questions

QuestionShort 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
cachingCDNhashingread-heavyanalytics