Design a URL Shortener (TinyURL/Bit.ly)

0/4 in this phase0/45 across the roadmap

📖 Concept

A URL shortener converts long URLs into short, shareable links. This is a classic system design interview question that tests fundamentals.

Requirements

Functional: Create short URL from long URL, redirect short URL to original, custom aliases (optional), link expiration, analytics (click count, geo)

Non-Functional: Very low latency for redirects (< 50ms), high availability (links must always work), 100M URLs created/month, 10:1 read:write ratio (1B redirects/month)

Back-of-Envelope Estimation

  • 100M new URLs/month ≈ 40 URLs/sec (write)
  • 1B redirects/month ≈ 400 redirects/sec (read)
  • Storage per URL: ~500 bytes → 100M × 500B = 50GB/month → 6TB over 10 years
  • Short URL length: 62^7 = 3.5 trillion combinations (base62: a-z, A-Z, 0-9)

High-Level Design

Client → API Gateway → URL Service → Database (write)
Client → CDN/Cache → URL Service → Database (redirect)

Key Design Decisions

ID Generation

  • Counter-based: Sequential IDs encoded to base62. Simple but predictable.
  • Hash-based: MD5/SHA256 of long URL, take first 7 chars. Collisions possible.
  • Pre-generated: Generate random IDs in advance, assign from pool. Fast, no collisions.

Storage

  • Database: Key-value (DynamoDB) or wide-column (Cassandra) — simple access pattern (get by short_code)
  • Cache: Redis for hot URLs (80/20 rule: 20% of URLs get 80% of traffic)

Redirect Type

  • 301 (Permanent): Browser caches, reduces server load but loses analytics
  • 302 (Temporary): Every redirect hits your server — better for analytics

💻 Code Example

codeTap to expand ⛶
1// ============================================
2// URL Shortener — System Design Implementation
3// ============================================
4
5class URLShortener {
6 constructor(db, cache, idGenerator) {
7 this.db = db;
8 this.cache = cache;
9 this.idGen = idGenerator;
10 this.BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
11 }
12
13 // Create short URL
14 async createShortURL(longURL, customAlias = null, expiresAt = null) {
15 // Check if URL already shortened (deduplication)
16 const existing = await this.db.findByLongURL(longURL);
17 if (existing && !customAlias) return existing.shortCode;
18
19 let shortCode;
20 if (customAlias) {
21 // Custom alias — check availability
22 const taken = await this.db.findByShortCode(customAlias);
23 if (taken) throw new Error('Alias already taken');
24 shortCode = customAlias;
25 } else {
26 // Generate unique short code
27 const id = await this.idGen.nextId();
28 shortCode = this.toBase62(id);
29 }
30
31 const url = {
32 shortCode,
33 longURL,
34 createdAt: new Date(),
35 expiresAt: expiresAt ? new Date(expiresAt) : null,
36 clickCount: 0,
37 };
38
39 await this.db.save(url);
40 await this.cache.set(`url:\${shortCode}`, longURL, 'EX', 86400);
41 return shortCode;
42 }
43
44 // Redirect: shortCode → longURL
45 async resolve(shortCode) {
46 // Check cache first (hot path)
47 let longURL = await this.cache.get(`url:\${shortCode}`);
48 if (longURL) {
49 this.recordClick(shortCode); // Async, don't block redirect
50 return longURL;
51 }
52
53 // Cache miss — query database
54 const url = await this.db.findByShortCode(shortCode);
55 if (!url) return null;
56 if (url.expiresAt && new Date() > url.expiresAt) return null;
57
58 // Populate cache
59 await this.cache.set(`url:\${shortCode}`, url.longURL, 'EX', 86400);
60 this.recordClick(shortCode);
61 return url.longURL;
62 }
63
64 toBase62(num) {
65 let result = '';
66 while (num > 0) {
67 result = this.BASE62[num % 62] + result;
68 num = Math.floor(num / 62);
69 }
70 return result.padStart(7, '0');
71 }
72
73 async recordClick(shortCode) {
74 // Fire-and-forget to analytics queue (don't block redirect)
75 this.cache.incr(`clicks:\${shortCode}`).catch(() => {});
76 }
77}
78
79// Demo
80const shortener = {
81 toBase62(num) {
82 const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
83 let result = '';
84 while (num > 0) { result = chars[num % 62] + result; num = Math.floor(num / 62); }
85 return result.padStart(7, '0');
86 }
87};
88console.log('Short code for ID 123456789:', shortener.toBase62(123456789));
89console.log('Total possible 7-char codes:', Math.pow(62, 7).toLocaleString());

🏋️ Practice Exercise

  1. Full Design: Design a URL shortener handling 100M URLs/month. Include: API design, database schema, caching layer, ID generation strategy, and analytics pipeline.

  2. Collision Handling: If using hash-based IDs, design a collision resolution strategy that maintains O(1) lookups.

  3. Analytics Dashboard: Design the analytics system for tracking click counts, geographic distribution, referrer, device type, and time-series click data.

  4. Rate Limiting: Design rate limits to prevent abuse: per-IP, per-user, and global limits for URL creation.

⚠️ Common Mistakes

  • Using MD5/SHA for short codes without collision handling — hash collisions WILL happen at scale. Always check for existence before saving.

  • Not caching hot URLs — most traffic goes to a small set of popular URLs. Cache the top 20% in Redis for sub-millisecond redirects.

  • Using 301 redirects — 301 is permanent; browsers cache it and never hit your server again. You lose ALL analytics data. Use 302 for tracking.

  • Not handling link expiration — without TTL cleanup, the database grows forever. Implement scheduled cleanup jobs for expired links.

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Design a URL Shortener (TinyURL/Bit.ly)