Design a URL Shortener (bit.ly)
Given 30-45 minutes in an interview, this is the most common system design question. Here is the full answer with every layer explained.
1. Requirements Clarification
- · Given a long URL, generate a short URL (alias)
- · Redirect short URL to original URL
- · Custom aliases (optional)
- · Expiry / TTL for links (optional)
- · Analytics: click count, geo, device (nice to have)
- · High availability — redirect must be available 24/7
- · Low latency — redirection in <10ms (with cache)
- · Scale — 100M new URLs/day write; 10B reads/day
- · Short URL must be unique
- · URL cannot be predictable/guessable
2. Capacity Estimation
3. API Design
/api/v1/shorten /{shortCode} /api/v1/{shortCode}/stats /api/v1/{shortCode} 301 (Permanent): Browser caches the redirect. Next time, browser goes directly to long URL — our server gets less traffic. Better for reduced load but we lose click analytics.
302 (Temporary): No caching. Every redirect hits our server — we can track analytics. Use 302 for analytics-heavy systems.
4. Short Code Generation — Trade-offs
How: MD5(longUrl) → take first 7 chars → Base62
→ Acceptable for small scale. Need collision handling.
How: DB auto-increment ID (e.g. 12345) → Base62 encode → 'dnh'
→ Best approach. Use distributed ID generator (Twitter Snowflake).
Characters: a-z (26) + A-Z (26) + 0-9 (10) = 62 chars.
7 characters: 62⁷ ≈ 3.5 trillion unique codes → enough for 182B URLs with room to spare.
5. High-Level Architecture
6. Database Schema
Why not just the short_code as PK? Auto-increment integer ID is used as the input to Base62 encoding. The short_code is a derived column, not the primary key.
7. Scaling Bottlenecks & Solutions
Single primary is fine for this write load (MySQL handles ~5K write QPS). Add connection pooling. Only shard when reaching 10K+ QPS.
95% of reads served from Redis cache. Only 5,800 QPS hits DB. Add read replicas to handle remaining. Cache key: shortCode → longUrl with TTL.
Pre-populate cache for popular URLs. Add jitter to TTL to prevent simultaneous expiry. Use pub-sub to invalidate cache on URL deletion.
Redis Cluster with replication. Master-replica with sentinel for automatic failover. Redis persistence (RDB + AOF) for durability.
Background job runs every hour. Lazy deletion: check expiry on read and return 404. Redis TTL handles cache expiry automatically.
8. Likely Follow-up Questions
A: User provides their preferred alias → check if it exists in DB (shortCode lookup) → if taken, return error → if free, insert with user's alias as short_code instead of auto-generated one. Limit custom aliases to premium users to prevent abuse.
A: Rate limiting per API key (e.g., 1000 URL creations per day). Use URL reputation service to check if long URL is malicious. Require email verification before allowing URL creation.
A: Don't block redirect with analytics writes. Use fire-and-forget: push click event to Kafka on redirect, then redirect immediately. Consumer processes events asynchronously and writes to a time-series DB (ClickHouse, InfluxDB) for analysis.
A: Shard by shortCode hash (consistent hashing). Each shard has its own auto-increment range (or use distributed ID gen like Twitter Snowflake). Cache layer remains the same. CDN can directly serve redirect for very popular URLs.