Design a URL Shortener: Step-by-Step System Design

The **URL Shortener** (such as TinyURL, bit.ly, or ow.ly) is the absolute classic question in system design interviews. While the core concept appears deceptively simple—take a long URL and return a short alias—designing a system that handles hundreds of millions of redirections with ultra-low latency, high availability, and massive write throughput requires deep understanding of distributed architectures, caching layers, and database partitioning.

This comprehensive, step-by-step guide breaks down how to design a production-grade URL shortener from scratch, covering requirements estimation, data modeling, encoding algorithms, database strategies, and scaling patterns.

Phase 1: Requirements Estimation and Capacity Planning

Before drawing architectural diagrams, you must establish functional and non-functional requirements alongside back-of-the-envelope estimations.

  • Functional Requirements: Given a long URL, the system generates a unique shorter alias. When a user visits the short link, they are HTTP redirected (301/302) to the original URL. Users should also be able to specify custom short links.
  • Non-Functional Requirements: Highly available, ultra-low latency redirection (< 10ms), short links must be non-guessable and secure.
  • Traffic Estimates: Assume 100 million new URL shortenings per month (Writes) and a 10:1 Read-to-Write ratio, equaling 1 billion redirects per month (Reads).
C
# Back-of-the-envelope calculations:
# Write QPS = 100,000,000 / (30 days * 24 hours * 3600 seconds) ≈ 38.5 requests/sec
# Read QPS = 1,000,000,000 / (30 days * 24 hours * 3600 seconds) ≈ 385 requests/sec
# Storage for 5 years = 100M/month * 60 months = 6 billion URLs
# Assuming each record is 500 bytes: 6B * 500 bytes = 3 TB of total storage over 5 years.

Phase 2: API Endpoints Design

Define clean RESTful endpoints for generating short URLs and handling redirections.

C
# 1. Create Short URL Endpoint
POST /api/v1/data/shorten
Request Body:
{
    "long_url": "https://www.codecrown.in/devops/tutorials/design-url-shortener",
    "custom_alias": "my-url-guide"
}
Response (201 Created):
{
    "short_url": "https://cc.in/my-url-guide",
    "expires_at": "2031-10-18T00:00:00Z"
}

# 2. Redirection Endpoint
GET /{short_alias}
Response (301 Moved Permanently or 302 Found):
Headers: Location: https://www.codecrown.in/devops/tutorials/design-url-shortener

Phase 3: Database Design and Data Modeling

Because the access pattern is read-heavy with simple key-value lookups (`short_hash` -> `long_url`), a relational database (PostgreSQL) or NoSQL key-value store (Cassandra, DynamoDB) works effectively.

C
-- PostgreSQL URL Table Schema
CREATE TABLE urls (
    id BIGSERIAL PRIMARY KEY,
    short_hash VARCHAR(10) UNIQUE NOT NULL,
    long_url TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    user_id BIGINT
);

-- Create index for lightning-fast hash lookups
CREATE INDEX idx_short_hash ON urls(short_hash);

Phase 4: Hash Generation and Base62 Encoding

How do we generate a compact 6 to 8 character short hash for long URLs? There are two primary approaches:

  • Approach 1: MD5 / SHA-256 Hashing: Hashing the long URL produces a 128/256-bit string. Taking the first 6-8 characters can lead to hash collisions. If a collision occurs, append a salt string and rehash.
  • Approach 2: Base62 Conversion (Recommended): Use an auto-incrementing database ID (e.g., `2000000000`) and convert it into a Base62 string using characters `[a-z, A-Z, 0-9]`. Base62 ensures zero collisions and perfectly compact short aliases.
C
// Python Base62 encoder snippet
alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def base62_encode(num):
    if num == 0:
        return alphabet[0]
    arr = []
    while num:
        num, rem = divmod(num, 62)
        arr.append(alphabet[rem])
    return ''.join(reversed(arr))

Phase 5: Caching and High-Availability Scaling

To achieve sub-10ms latency under 1,000+ Read QPS, caching is mandatory.

  • Redis Caching Layer: Place Redis in front of the database. When a redirection request arrives, check Redis first. If a cache miss occurs, query the PostgreSQL database, populate Redis, and return the result.
  • Cache Eviction Policy: Use Least Recently Used (LRU) eviction so that viral, frequently accessed short links remain in memory.
  • Load Balancing: Deploy multiple stateless web servers behind an Nginx or AWS Application Load Balancer (ALB) to distribute incoming traffic smoothly.

Conclusion

Designing a URL shortener requires balancing storage efficiency, collision-free hash generation via Base62 encoding, and low-latency read performance through Redis caching. Mastering these distributed systems principles prepares engineers to tackle complex enterprise scaling challenges.