Design a URL Shortener (TinyURL/Bit.ly)
📖 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
1// ============================================2// URL Shortener — System Design Implementation3// ============================================45class URLShortener {6 constructor(db, cache, idGenerator) {7 this.db = db;8 this.cache = cache;9 this.idGen = idGenerator;10 this.BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';11 }1213 // Create short URL14 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;1819 let shortCode;20 if (customAlias) {21 // Custom alias — check availability22 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 code27 const id = await this.idGen.nextId();28 shortCode = this.toBase62(id);29 }3031 const url = {32 shortCode,33 longURL,34 createdAt: new Date(),35 expiresAt: expiresAt ? new Date(expiresAt) : null,36 clickCount: 0,37 };3839 await this.db.save(url);40 await this.cache.set(`url:\${shortCode}`, longURL, 'EX', 86400);41 return shortCode;42 }4344 // Redirect: shortCode → longURL45 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 redirect50 return longURL;51 }5253 // Cache miss — query database54 const url = await this.db.findByShortCode(shortCode);55 if (!url) return null;56 if (url.expiresAt && new Date() > url.expiresAt) return null;5758 // Populate cache59 await this.cache.set(`url:\${shortCode}`, url.longURL, 'EX', 86400);60 this.recordClick(shortCode);61 return url.longURL;62 }6364 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 }7273 async recordClick(shortCode) {74 // Fire-and-forget to analytics queue (don't block redirect)75 this.cache.incr(`clicks:\${shortCode}`).catch(() => {});76 }77}7879// Demo80const 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
Full Design: Design a URL shortener handling 100M URLs/month. Include: API design, database schema, caching layer, ID generation strategy, and analytics pipeline.
Collision Handling: If using hash-based IDs, design a collision resolution strategy that maintains O(1) lookups.
Analytics Dashboard: Design the analytics system for tracking click counts, geographic distribution, referrer, device type, and time-series click data.
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)