Design a URL Shortener
Difficulty: ββ | Asked at: Google, Amazon, Meta, Microsoft | Time: 45 minutes
π― Problem Statement
Design a URL shortening service like TinyURL or bit.ly that:
- Shortens long URLs to compact aliases
- Redirects short URLs to original URLs
- Handles high traffic with low latency
Step 1: Requirements
Functional Requirements
- Given a long URL, generate a short, unique URL
- Given a short URL, redirect to the original long URL
- Users can optionally set custom short URLs
- Links expire after a configurable time (default: 5 years)
- Users can view click analytics (click count, referrers, geography)
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Availability | 99.99% |
| Latency (redirect) | < 100ms |
| Throughput | 100M URLs/day created |
| Read:Write ratio | 100:1 (redirects far exceed creation) |
| Durability | URLs never lost |
Capacity Estimation
Write: 100M URLs/day = ~1,160 writes/sec
Read: 100:1 ratio = ~116,000 reads/sec
Peak: 2x average = ~2,320 writes/sec, ~232,000 reads/sec
Storage (5 years):
100M Γ 365 Γ 5 = 182.5B records
182.5B Γ 500 bytes = ~91 TB
Bandwidth:
Write: 1,160 Γ 500B = ~580 KB/s
Read: 116,000 Γ 500B = ~58 MB/s
Cache (80/20 rule - 20% of URLs get 80% of traffic):
Daily active URLs: ~20M unique
Cache size: 20M Γ 500B = ~10 GB
Step 2: High-Level Design
Architecture
ββββββββββββ ββββββββββββββββ βββββββββββββββββ
β Client βββββββ Load Balancerβββββββ API Servers β
ββββββββββββ ββββββββββββββββ βββββββββ¬ββββββββ
β
βββββββββββββββββββββββββββΌβββββββββββββββββ
β β β
βββββββΌβββββββ ββββββββΌββββββββ ββββββΌββββββ
β Cache β β Database β βAnalytics β
β (Redis) β β (PostgreSQL) β β (Kafka) β
ββββββββββββββ ββββββββββββββββ ββββββββββββ
API Design
POST /api/v1/urls
Body: { "long_url": "https://example.com/very/long/path",
"custom_alias": "my-link", // optional
"expires_in_days": 365 } // optional, default 1825
Response: { "short_url": "https://short.ly/abc123",
"expires_at": "2026-08-02T00:00:00Z" }
GET /{short_code}
Response: 301 Redirect β long_url
(Use 301 for permanent, 302 for temporary β affects browser caching)
GET /api/v1/urls/{short_code}/analytics
Response: { "total_clicks": 12345,
"unique_visitors": 8901,
"clicks_by_date": { "2025-01-01": 100, ... },
"top_referrers": { "google.com": 500, ... },
"top_countries": { "US": 3000, "IN": 2000, ... } }
Database Schema
CREATE TABLE urls (
id BIGSERIAL PRIMARY KEY,
short_code VARCHAR(10) UNIQUE NOT NULL,
long_url TEXT NOT NULL,
user_id BIGINT REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
is_custom BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_short_code ON urls(short_code);
CREATE INDEX idx_expires_at ON urls(expires_at);
CREATE TABLE click_events (
id BIGSERIAL PRIMARY KEY,
short_code VARCHAR(10) NOT NULL,
clicked_at TIMESTAMP DEFAULT NOW(),
ip_address INET,
user_agent TEXT,
referrer TEXT,
country_code VARCHAR(2)
);
CREATE INDEX idx_click_code ON click_events(short_code);
CREATE INDEX idx_click_time ON click_events(clicked_at);
Step 3: Deep Dive
URL Shortening Algorithm
Approach 1: Hash + Truncate
import hashlib
import base64
def generate_short_code(long_url, length=7):
hash_bytes = hashlib.md5(long_url.encode()).digest()
short_code = base64.urlsafe_b64encode(hash_bytes).decode()[:length]
return short_code
# Problem: Collision possible
# Solution: Check DB, if collision β append counter or use different hash
Approach 2: Base62 Encoding of Auto-Increment ID
CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def id_to_short_code(id):
if id == 0:
return CHARS[0]
code = []
while id > 0:
code.append(CHARS[id % 62])
id //= 62
return ''.join(reversed(code))
# ID 12345 β short code "3d7"
# Pros: No collision, deterministic
# Cons: Predictable (security concern), requires distributed ID generation
Approach 3: Pre-Generated Key Service (Recommended)
βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ
β Key Service βββββββ Key Store βββββββ API Server β
β (generates keys β β (available keys)β β (assigns key β
β in batches) β β β β to URL) β
βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ
- Pre-generate millions of random 7-char keys (Base62)
- Store in DB table: keys(code, is_used)
- API server fetches batch of 1000 unused keys
- When batch runs low, fetch more
- Pros: No collision, not predictable, fast
Caching Strategy
Cache Design (Redis):
βββ Key: short_code
βββ Value: long_url
βββ TTL: 24 hours (refreshed on access)
βββ Eviction: LRU
βββ Size: ~10 GB for hot URLs
Read Path:
1. Client β GET /abc123
2. API Server β Check Redis cache
3. Cache HIT β Return 301 redirect (fast path)
4. Cache MISS β Query PostgreSQL
5. Store in Redis β Return 301 redirect
Write Path:
1. Client β POST /api/v1/urls
2. API Server β Generate short code
3. Write to PostgreSQL
4. Write to Redis cache
5. Return short URL
Cache Invalidation:
- On URL deletion: Delete from Redis
- On URL update: Delete from Redis (lazy reload)
- TTL expiration: Automatic cleanup
Database Scaling
Read Replicas:
ββββββββββββββββ
β Primary βββββ Writes
β (PostgreSQL)β
ββββββββ¬ββββββββ
β Replication
ββββββΌβββββ¬βββββββββ
β β β β
βββΌβββββΌβββββΌββ ββββΌβββ
βR1 ββR2 ββR3 β β R4 β β Reads (4 replicas)
βββββββββββββββ βββββββ
Sharding Strategy (if needed at scale):
- Shard by short_code hash
- Consistent hashing for even distribution
- Cross-shard queries avoided by design
Handling Expired URLs
Approach 1: Lazy Cleanup (Recommended)
- On read: Check expires_at β If expired, return 404 + delete
- Background job: Periodically scan and delete expired URLs
- Pros: Simple, no extra infrastructure
- Cons: Stale data in DB until accessed
Approach 2: TTL-based (Redis handles expiration)
- Set Redis TTL = expires_at - now()
- Background job cleans PostgreSQL
- Pros: Automatic expiration in cache
- Cons: Two systems to manage
Step 4: Trade-offs
301 vs 302 Redirect
| Code | Meaning | Browser Behavior | Use Case |
|---|---|---|---|
| 301 | Permanent | Caches redirect | SEO, permanent links |
| 302 | Temporary | Always asks server | Analytics, temporary links |
Recommendation: Use 302 if you need accurate analytics (every request hits your server).
Hash vs Sequential ID
| Approach | Pros | Cons |
|---|---|---|
| Hash + Truncate | No extra service | Collision possible |
| Sequential ID | No collision | Predictable, requires distributed ID |
| Pre-generated keys | No collision, secure | Extra service to maintain |
Synchronous vs Async Analytics
| Approach | Pros | Cons |
|---|---|---|
| Synchronous write | Simple, immediate | Slows down redirect |
| Async (Kafka) | Fast redirect, decoupled | Eventual consistency |
Recommendation: Async β redirect should be as fast as possible.
π Monitoring & Reliability
Key Metrics to Monitor:
βββ Redirect latency (p50, p95, p99)
βββ Cache hit ratio (target: > 95%)
βββ URL creation rate
βββ Error rate (4xx, 5xx)
βββ Database connection pool usage
βββ Kafka consumer lag
Alerting:
βββ Redirect latency p99 > 200ms β Warning
βββ Cache hit ratio < 90% β Investigate
βββ Error rate > 1% β Critical
βββ Database replication lag > 5s β Warning
π Cross-References
- Rate Limiter β Protect the URL shortener from abuse
- Key-Value Store β Deep dive on distributed storage
- Caching Concepts β Caching strategies
- Database Questions β SQL vs NoSQL trade-offs