Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Given a long URL, generate a short, unique URL
  2. Given a short URL, redirect to the original long URL
  3. Users can optionally set custom short URLs
  4. Links expire after a configurable time (default: 5 years)
  5. Users can view click analytics (click count, referrers, geography)

Non-Functional Requirements

RequirementTarget
Availability99.99%
Latency (redirect)< 100ms
Throughput100M URLs/day created
Read:Write ratio100:1 (redirects far exceed creation)
DurabilityURLs 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

CodeMeaningBrowser BehaviorUse Case
301PermanentCaches redirectSEO, permanent links
302TemporaryAlways asks serverAnalytics, temporary links

Recommendation: Use 302 if you need accurate analytics (every request hits your server).

Hash vs Sequential ID

ApproachProsCons
Hash + TruncateNo extra serviceCollision possible
Sequential IDNo collisionPredictable, requires distributed ID
Pre-generated keysNo collision, secureExtra service to maintain

Synchronous vs Async Analytics

ApproachProsCons
Synchronous writeSimple, immediateSlows down redirect
Async (Kafka)Fast redirect, decoupledEventual 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