Skip to content

URL Shortener Design — Advanced Architecture and Performance Optimization

DodaTech Updated 2026-06-22 7 min read

In this tutorial, you'll learn about URL Shortener Design. We cover key concepts, practical examples, and best practices.

Designing a URL shortener requires building a globally distributed system that handles millions of writes per day, billions of redirects, and provides sub-20 millisecond response times with strong consistency guarantees.

What You'll Learn

You'll master distributed ID generation with Snowflake, Bloom filter-based duplicate detection, hot-key caching strategies with consistent hashing, real-time analytics pipelines, and multi-region active-active deployment patterns.

Why It Matters

URL shorteners sit at the intersection of every link shared online. At TinyURL scale, 5 billion redirects per day means every millisecond of latency costs user engagement. At DodaTech, similar redirection patterns optimize deep linking in Doda Browser's privacy-focused link forwarding layer.

Real-World Use

Bitly processes 8 billion clicks monthly across 1.5 million customers. Each click generates analytics — referrer, device, location, time — feeding real-time dashboards. A single millisecond improvement in redirect latency saves 800,000 cumulative hours of user waiting per month.

System Architecture

flowchart TB
    Client[User/Bot] -->|Short URL click| DNS[DNS Geo-Routing]
    DNS --> LB[Global Load Balancer]
    LB --> Regional[Regional Cluster]
    subgraph Regional[Regional Deployment]
        GW[API Gateway] --> Write[Write Service]
        GW --> Redirect[Redirect Service]
        GW --> Analytics[Analytics Service]
        Write --> IDGen[Snowflake ID Gen]
        Write --> Blocker[Bloom Filter]
        Write --> DB[(CockroachDB Multi-Region)]
        Redirect --> Cache[(Redis Cluster)]
        Cache --> DB
        Blocker --> Cache
        Analytics --> Kafka[Kafka Stream]
        Kafka --> StreamProc[Stream Processor]
        StreamProc --> OLAP[(ClickHouse)]
    end

Distributed ID Generation

Unlike the simple auto-increment approach, production URL shorteners use Snowflake-style IDs to avoid single-point-of-failure bottlenecks and enable multi-region writes.

import time
import threading

class SnowflakeGenerator:
    def __init__(self, datacenter_id: int, worker_id: int):
        self.datacenter_id = datacenter_id
        self.worker_id = worker_id
        self.sequence = 0
        self.last_timestamp = -1
        self.lock = threading.Lock()
        self.epoch = 1700000000000

    def next_id(self) -> int:
        with self.lock:
            timestamp = int(time.time() * 1000) - self.epoch
            if timestamp < self.last_timestamp:
                raise Exception("Clock moved backwards")
            if timestamp == self.last_timestamp:
                self.sequence = (self.sequence + 1) & 4095
                if self.sequence == 0:
                    timestamp = self._wait_next_ms()
            else:
                self.sequence = 0
            self.last_timestamp = timestamp
            return (timestamp << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence

    def _wait_next_ms(self):
        ts = int(time.time() * 1000) - self.epoch
        while ts <= self.last_timestamp:
            ts = int(time.time() * 1000) - self.epoch
        return ts

Expected behavior: Each generator produces 4 million unique IDs per second per worker without coordination. IDs are globally unique, time-sortable, and 64-bit integers that encode directly to 7-character Base62 strings.

Bloom Filter for Duplicate Detection

Checking for duplicate URLs with a database query every time is expensive at 10,000 writes per second. A Bloom filter provides probabilistic duplicate detection with constant memory.

import mmh3
import math

class BloomFilter:
    def __init__(self, capacity: int = 100_000_000, error_rate: float = 0.001):
        self.size = int(-capacity * math.log(error_rate) / (math.log(2) ** 2))
        self.hash_count = int(self.size / capacity * math.log(2))
        self.bit_array = bytearray(math.ceil(self.size / 8))

    def _hashes(self, item: str):
        h1 = mmh3.hash128(item, 0)
        h2 = mmh3.hash128(item, 1)
        for i in range(self.hash_count):
            yield (h1 + i * h2) % self.size

    def add(self, item: str):
        for h in self._hashes(item):
            self.bit_array[h // 8] |= 1 << (h % 8)

    def might_contain(self, item: str) -> bool:
        for h in self._hashes(item):
            if not (self.bit_array[h // 8] & (1 << (h % 8))):
                return False
        return True

bloom = BloomFilter()
shortened_urls = {}

def shorten_url(long_url: str) -> str:
    if bloom.might_contain(long_url):
        existing = shortened_urls.get(long_url)
        if existing:
            return existing
    url_id = snowflake.next_id()
    short_code = base62_encode(url_id)
    shortened_urls[long_url] = short_code
    bloom.add(long_url)
    db.execute("INSERT INTO urls (id, short_code, long_url) VALUES (%s, %s, %s)", url_id, short_code, long_url)
    return short_code

Expected behavior: The Bloom filter uses approximately 170 MB for 100 million URLs with 0.1 percent false positive rate. Every URL is checked in microseconds without touching the database.

Hot-Key Caching with Consistent Hashing

Some short URLs receive millions of clicks (celebrity links, Super Bowl ads). These hot keys need dedicated cache slots to avoid cache stampedes.

import hashlib

class ConsistentHashRing:
    def __init__(self, nodes: list[str], replicas: int = 150):
        self.replicas = replicas
        self.ring = {}
        self.sorted_keys = []
        for node in nodes:
            for i in range(replicas):
                key = self._hash(f"{node}:{i}")
                self.ring[key] = node
                self.sorted_keys.append(key)
        self.sorted_keys.sort()

    def _hash(self, key: str) -> int:
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def get_node(self, key: str) -> str:
        if not self.sorted_keys:
            return None
        h = self._hash(key)
        for ring_key in self.sorted_keys:
            if ring_key >= h:
                return self.ring[ring_key]
        return self.ring[self.sorted_keys[0]]

    def get_hot_key_nodes(self, hot_keys: list[str]) -> dict:
        return {k: self.get_node(f"hot:{k}") for k in hot_keys}

ring = ConsistentHashRing(["cache-1:6379", "cache-2:6379", "cache-3:6379"])
hot_keys = ["superbowl-ad", "election-results", "product-launch"]
print(ring.get_hot_key_nodes(hot_keys))

Expected behavior: Hot keys are isolated to dedicated cache nodes to prevent popular URLs from evicting less popular ones. Consistent hashing minimizes redistribution when nodes are added or removed.

Redirect Response Strategy

Status Code Browser Cache Analytics Use Case
301 Moved Permanently Cached indefinitely Lost after first hit Permanent links, no analytics needed
302 Found Not cached Every click recorded Analytics tracking, A/B testing
307 Temporary Redirect Not cached, preserves method Every click recorded POST-based APIs

Production URL shorteners default to 302 for analytics and switch to 301 after 30 days for active links that have stable traffic patterns sorted.

Analytics Pipeline

-- Click events streamed via Kafka
CREATE TABLE click_events (
    short_code VARCHAR(10),
    timestamp DateTime,
    referrer String,
    user_agent String,
    ip_address String,
    country String,
    device_type String,
    browser String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (short_code, timestamp);

-- Materialized view for per-link daily stats
CREATE MATERIALIZED VIEW link_daily_stats
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (short_code, day)
AS SELECT
    short_code,
    toDate(timestamp) AS day,
    count() AS clicks,
    uniq(ip_address) AS unique_visitors,
    uniq(referrer) AS unique_referrers
FROM click_events
GROUP BY short_code, day;

Common Errors

1. Synchronous ID Generation

Auto-increment databases become write bottlenecks. Distributed Snowflake generators run on every write node without coordination.

2. Missing Bloom Filter Fallback

Bloom filters have false positives. Always verify with a database lookup when the filter reports a potential duplicate.

3. Single-Region Deployment

DNS-level redirects from Australia to US-East add 150ms latency. Deploy active-active in at least three regions.

4. Cache-Aside Without Hot Key Detection

Without DCP (distributed cache population) patterns for hot keys, every celebrity link causes a cache stampede that overloads the database.

5. Analytics in the Critical Path

Writing click data synchronously during redirects doubles latency. Use async Kafka producers with local buffering.

6. No Rate Limiting

Without rate limiting, malicious users can exhaust the ID space or overload the system. Limit to 10 shorten requests per IP per second using a sliding window counter.

7. Ignoring Custom Alias Validation

Custom aliases like "admin", "api", or "login" must be reserved. Validate against a reserved-word set during alias creation.

Practice Questions

1. Why use Snowflake IDs over auto-increment for URL shorteners?

Auto-increment requires a single database master, creating a single point of failure and limiting write throughput. Snowflake generates unique IDs across thousands of nodes without coordination, supporting multi-region active-active writes.

2. How does a Bloom filter help with duplicate URL detection?

A Bloom filter provides a memory-efficient, probabilistic membership test. It eliminates 99.9 percent of database lookups for URLs that have never been shortened, reducing write path latency from 10ms to microseconds.

3. What is the difference between 301 and 302 redirects in this context?

301 responses are cached by browsers forever after the first hit, reducing server load but losing analytics. 302 responses always hit the server, enabling click tracking but increasing redirect latency by approximately 5-10ms.

4. How do you handle hot-key cache stampedes?

Detect hot keys by tracking access frequency, promote them to dedicated cache nodes, pre-warm on predicted traffic spikes, and implement a circuit breaker that falls back to database reads if the cache tier is overloaded.

5. Challenge: Implement a distributed rate limiter for the shorten endpoint that uses a sliding window counter stored in Redis sorted sets. Each IP address gets 10 requests per 60-second window. Return HTTP 429 when exceeded.

Mini Project: URL Shortener Analytics Dashboard

Build a complete URL shortener with:

  1. A Snowflake ID generator service exposing a gRPC GetId method
  2. A write service with Bloom filter duplicate detection, rate limiting, and async analytics logging
  3. A redirect service with consistent hashing-based hot key caching
  4. A ClickHouse-backed analytics service with daily materialized views
  5. A dashboard showing per-link click counts, unique visitors, referrer breakdown, and geographic distribution

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro