Design a URL shortener like bit.ly: ID generation, hashing, redirection, analytics, and scaling.
Published April 20, 2025
Functional:
Non-functional:
Option 1: Hash + truncate
String shortCode = Base62.encode(MD5(longUrl).substring(0, 8));
// Problem: collisions possible when truncating
Option 2: Auto-increment ID + Base62 (recommended)
long id = idGenerator.nextId(); // e.g., Snowflake ID or DB auto-increment
String shortCode = base62Encode(id);
// 62^7 = 3.5 trillion codes — sufficient
String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
String base62Encode(long id) {
StringBuilder sb = new StringBuilder();
while (id > 0) { sb.insert(0, BASE62.charAt((int)(id % 62))); id /= 62; }
return sb.toString();
}
CREATE TABLE urls (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(10) UNIQUE NOT NULL,
long_url VARCHAR(2048) NOT NULL,
user_id BIGINT,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
click_count BIGINT DEFAULT 0
);
CREATE INDEX idx_short_code ON urls(short_code); -- critical for redirects
Client
↓
CDN (cache popular short codes)
↓
Load Balancer
↓
[Redirect Service] [Shorten Service]
↓ ↓
[Redis Cache] [ID Generator (Snowflake)]
↓ ↓
[MySQL/DynamoDB] ← [MySQL Write Master]
(read replicas) ↓
[Analytics Queue → ClickHouse]
GET /abc123
1. Check Redis cache: key="url:abc123"
2. Cache hit → return 301/302 redirect
3. Cache miss → query DB → cache result → return redirect
4. Async: publish click event to message queue
301 vs 302: