The Problem Statement
Design a URL shortening service like Bit.ly that:
- Takes a long URL and returns a short, unique URL
- Redirects short URLs to the original long URL
- Handles 100 million URLs per month (write-heavy)
- Short URLs should be as short as possible
- Supports analytics (click count, geographic data)
- High availability with minimal latency
This is one of the most common system design interview questions. Let's build it step by step.
Step 1: API Design
Core Endpoints
POST /api/v1/shorten
Body: { "long_url": "https://example.com/very/long/path", "custom_alias": "mylink" }
Response: { "short_url": "https://sho.rt/abc123", "expires_at": "2026-01-01" }
GET /{short_code}
Response: 301 Redirect to long_url
GET /api/v1/stats/{short_code}
Response: { "clicks": 15420, "created_at": "...", "top_countries": [...] }
Why 301 vs 302 Redirect?
- 301 (Permanent) — browser caches the redirect. Reduces server load but you lose analytics on repeat visits.
- 302 (Temporary) — every visit hits your server. Better for analytics. Use this.
Step 2: Encoding Strategy
The core problem: how do you generate a short, unique code?
Option A: Base62 Encoding
Use characters[a-zA-Z0-9] = 62 characters.
| Length | Combinations | Enough for |
|---|---|---|
| 6 | 56.8 billion | Most use cases |
| 7 | 3.5 trillion | Planet-scale |
Option B: MD5/SHA256 Hash + Truncate
Hash the long URL, take the first 7 characters. Problem: collisions. You need collision resolution.Option C: Auto-Increment ID + Base62
Use a database auto-increment counter, then convert to base62:CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(num):
if num == 0: return CHARSET[0]
result = []
while num > 0:
result.append(CHARSET[num % 62])
num //= 62
return ''.join(reversed(result))
ID 1000000 -> "4c92"
ID 56800235583 -> "zzzzzzz"
Best approach: Option C — simple, no collisions, predictable length.
Step 3: Database Schema
CREATE TABLE urls (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(10) UNIQUE NOT NULL,
long_url TEXT NOT NULL,
user_id BIGINT NULL,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NULL,
click_count BIGINT DEFAULT 0,
INDEX idx_short_code (short_code),
INDEX idx_user_id (user_id)
);
CREATE TABLE click_events (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
url_id BIGINT NOT NULL,
clicked_at TIMESTAMP DEFAULT NOW(),
ip_address VARCHAR(45),
user_agent TEXT,
country_code VARCHAR(2),
referrer TEXT,
INDEX idx_url_id (url_id),
INDEX idx_clicked_at (clicked_at)
);
Database Choice
- URLs table: MySQL or PostgreSQL — structured data, needs ACID transactions
- Click events: Append-only, write-heavy → Consider Cassandra or ClickHouse for analytics at scale
Step 4: Caching Layer
Most URL shorteners follow the 80/20 rule — 20% of URLs get 80% of traffic.
Cache Strategy
- Use Redis as a read-through cache
- Cache the mapping:
short_code → long_url - TTL: 24 hours for hot URLs
- Cache eviction: LRU (Least Recently Used)
Request flow:
User hits GET /abc123
Check Redis cache → HIT → redirect (< 1ms)
Cache MISS → query database → store in Redis → redirect
Expected cache hit ratio: 80–90% for popular URLs.
Step 5: Rate Limiting
Prevent abuse (spam URL creation, DDoS):
- Anonymous users: 10 URLs per hour per IP
- Authenticated users: 100 URLs per hour
- Implementation: Token bucket algorithm with Redis
def rate_limit(user_id, limit=100, window=3600):
key = f"rate:{user_id}"
current = redis.incr(key)
if current == 1:
redis.expire(key, window)
return current <= limit
Step 6: Scaling Architecture
Read Path (High Volume)
Client → CDN/Load Balancer → API Servers (stateless)
↓
Redis Cache
↓ (miss)
Read Replicas (MySQL)
Write Path
Client → Load Balancer → API Server → Primary DB (MySQL)
↓
ID Generator (Snowflake/Auto-increment)
↓
Async: Kafka → Analytics Pipeline
Key Scaling Decisions
Step 7: High Availability
- Multi-region deployment with DNS-based routing
- Database replication across availability zones
- Graceful degradation — if analytics fails, redirects still work
- Health checks — load balancer removes unhealthy instances
Capacity Estimation
| Metric | Value |
|---|---|
| New URLs / month | 100 million |
| Redirects / month | 10 billion (100:1 read:write) |
| Redirects / second | ~3,800 |
| Storage / URL | ~500 bytes |
| Storage / year | ~600 GB |
| Cache size (20% hot) | ~120 GB → fits in Redis |
Interview Tips
This question tests your ability to think at scale. Practice articulating your decisions clearly, and you'll nail it.
Good luck with your interviews! 🎯