Design a URL Shortener (TinyURL / Bitly)
Architect a global URL shortener: Base62 encoding vs MD5 hashing, collision handling, Range-Based Counter Token Servers (ZooKeeper), and Redis caching.
Global High-Availability URL Shortener Architecture ๐
Range-Based Counter Token allocation with ZooKeeper coordinator, multi-region Redis cache, and decoupled click analytics.
01.1. Functional & Non-Functional Requirements
A URL shortener converts arbitrary-length URLs into compact, human-readable aliases and redirects incoming traffic to the original destination with minimal latency.
Functional Requirements
- URL Shortening: Given a long URL (e.g.,
https://en.wikipedia.org/wiki/Distributed_computing), generate an alias of 7 characters (e.g.,https://tiny.url/4bY7a9Z). - High-Speed Redirection: When a client accesses the short alias, immediately redirect them to the original destination.
- Custom Aliases (Vanity URLs): Allow users to optionally supply custom alphanumeric slugs (e.g.,
https://tiny.url/my-promo). - Link Expiration & TTL: Support default retention periods (e.g., 5 years) and user-configurable expiration dates.
- Click Analytics: Asynchronously capture telemetry: timestamp, IP geolocation, user agent, referrer, and cumulative click count.
Non-Functional Requirements
- Ultra-Low Latency: Redirection lookups must complete in
< 10 ms(p99) from cache and< 30 mson persistent store cache misses. - High Availability & Durability:
99.999\%uptime for read operations. Saved URL mappings must never be lost. - Collision-Free Generation: ID generation must guarantee zero duplicate short URLs under concurrent multi-region writes.
- Security & Guess-Resistance: Short keys should not be trivially sequential to prevent malicious scraping of private links.
02.2. Back-of-the-Envelope Capacity Estimation
Understanding read-to-write ratios and storage footprints dictates the caching and database partitioning strategy:
Traffic Estimation
- Write Volume:
100 millionnew URLs created per month.
Write QPS = \frac{100,000,000}{30 ร 86,400} โ 38.6 โ 40 writes/sec (Peak: 100 writes/sec)
- Read Volume: 100:1 Read-to-Write ratio
\implies 10 billionredirects per month.
Read QPS = \frac{10,000,000,000}{30 ร 86,400} โ 3,858 โ 4,000 reads/sec (Peak: 15,000 reads/sec)
Storage Calculations (5-Year Horizon)
- Record Size:
short_key: 7 bytesoriginal_url: 512 bytes averageuser_id: 16 bytes (UUID)created_at,expires_at: 16 bytes- Overhead & indexing: ~50 bytes
- Total per record:
~ 600 bytes
- Total Storage:
600 bytes ร 100M/mo ร 12 mo/yr ร 5 yr = 3.6 Terabytes (TB)
Memory & Cache Sizing (80/20 Rule)
- 20% of the daily hot URLs account for 80% of daily redirect traffic:
Daily Reads = \frac{10B}{30} โ 333 Million requests/day
Hot 20% Volume = 333M ร 0.20 = 66.6 Million URLs
RAM Needed = 66.6M ร 600 bytes โ 40 GB of RAM
A modest Redis Cluster easily accommodates 40 GB in RAM.
03.3. Data Model & Database Schema
Since URL mappings are key-value lookups with no complex relational joins, either a sharded relational database (PostgreSQL/Aurora) or a distributed NoSQL document/wide-column store (DynamoDB/Cassandra/MongoDB) can be used.
Relational / Document Schema
sqlCREATE TABLE url_mappings ( short_key VARCHAR(10) PRIMARY KEY, original_url TEXT NOT NULL, user_id UUID, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE, is_custom BOOLEAN DEFAULT FALSE, click_count BIGINT DEFAULT 0 ); CREATE INDEX idx_user_id ON url_mappings(user_id); CREATE INDEX idx_expires_at ON url_mappings(expires_at) WHERE expires_at IS NOT NULL;
Sharding Strategy
- Partition Key:
short_keyhash partition using Consistent Hashing across database shards. This distributes read traffic uniformly with zero hotspotting.
04.4. API Design & HTTP Redirect Semantics
Endpoints
-
Create Short URL
POST /api/v1/urls- Request:
json
{ "original_url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301", "custom_alias": "mdn-301", "expires_in_days": 30 } - Response (201 Created):
json
{ "short_url": "https://tiny.url/mdn-301", "original_url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301", "created_at": "2026-09-27T10:00:00Z", "expires_at": "2026-10-27T10:00:00Z" }
-
Redirect to Long URL
GET /{short_key}- Headers:
Location: https://developer.mozilla.org/... - Status Code:
301 Moved Permanentlyvs302 Found/307 Temporary Redirect
[!IMPORTANT] HTTP 301 vs HTTP 302 in System Design:
- 301 Moved Permanently: The client browser permanently caches the redirect. Subsequent clicks never touch our servers, minimizing server load. Drawback: We lose click tracking and real-time analytics.
- 302 Found (or 307 Temporary Redirect): The browser sends every redirect request to our server first. Advantage: Enables
100\%accurate click telemetry, geographic analytics, and dynamic redirect rule execution.
05.5. Key Generation Algorithms: MD5 Hashing vs Range-Based Counters
We need a 7-character string using Base62 characters ([a-z, A-Z, 0-9]).
62^7 = 3,521,614,606,208 โ 3.52 Trillion unique URLs
Approach 1: Cryptographic Hash + Truncate (Flawed)
- Calculate
MD5(original_url)orSHA-256(original_url)and convert the first 43 bits to 7 Base62 characters. - Problem: Hash collisions! If two distinct long URLs produce the same 7-character prefix, we must query the database to detect collision, append a salt/nonce, and re-hash. Under high write loads, this creates quadratic database overhead.
Approach 2: Range-Based Distributed Counter (The Winning Senior Architecture)
Instead of hashing strings, use a monotonically increasing integer counter converted directly to Base62 (e.g., Integer 1,000,042 \implies Base62 4bY7).
How ZooKeeper Pre-Allocates Counter Ranges:
- A central coordinator (Apache ZooKeeper or etcd) manages global integer blocks.
- When App Server 1 boots, it requests a token range from ZooKeeper (e.g., Range
1,000,000 - 1,999,999). - App Server 2 requests the next block (Range
2,000,000 - 2,999,999). - Each App Server increments its local counter in atomic CPU memory (
AtomicLong) with zero network round trips. - When a server exhausts its 1M range, it fetches a fresh block from ZooKeeper.
- Result: Guarantees zero collisions, zero database checks, and sub-millisecond generation speed.
typescript// Base62 Conversion Algorithm const BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; function encodeBase62(num: bigint): string { let encoded = ""; while (num > 0n) { const remainder = Number(num % 62n); encoded = BASE62[remainder] + encoded; num = num / 62n; } return encoded.padStart(7, "0"); }
06.6. Deep Dives: Obfuscation, Caching & Expired URL Purging
1. Counter Predictability & Security Mitigation
If integers increment sequentially (1000000, 1000001), attackers can enumerate all valid URLs by traversing Base62 values.
- Mitigation: Use a Shuffled Base62 Lookup Alphabet or pass the integer through a lightweight Feistel cipher (Format-Preserving Encryption) before Base62 encoding to produce pseudo-random yet collision-free slugs.
2. Multi-Tier Caching
- Edge CDN (Cloudflare Workers / Fastly): Caches popular 301/302 redirects with short TTLs (e.g., 10 minutes) at Points of Presence worldwide.
- Application Cache (Redis Cluster): Uses Least Recently Used (LRU) eviction policy. A write-through or cache-aside strategy ensures popular URLs are served from RAM in
< 1 ms.
3. Asynchronous Expiration Sweeper
- Passive deletion: When a requested URL is found to have
expires_at < NOW(), return HTTP 404 and trigger background cleanup. - Active cleanup: A scheduled cron/worker sweeps expired records in time-bucketed batches during low-traffic off-peak hours to reclaim database storage.
โ๏ธArchitectural Trade-offs & Production Realities
Architectural Advantages
- Range-based Base62 eliminates database collision checks entirely with zero lock contention
- Redis cluster caches 99% of hot redirects in RAM, reducing p99 response times to < 5ms
- Base62 encoding of 7 characters provides massive capacity (3.52 Trillion URLs)
Trade-offs & Constraints
- Sequential integer counters require obfuscation/Feistel cipher to prevent URL enumeration scraping
- ZooKeeper coordinator crash stops new range allocations (mitigated by standby ZooKeeper quorum)
Bitly handles tens of billions of monthly clicks using distributed range-based key generation services and aggressive edge caching to return HTTP 301/302 redirects in under 15ms globally.
๐ฏ Staff+ Engineering Takeaways
- Base62 encoding of 7 characters yields 3.52 Trillion unique, compact URL slugs.
- Range-based distributed counters pre-allocated by ZooKeeper eliminate hash collision lookups completely.
- Use HTTP 301 to offload traffic to browser caches, or HTTP 302/307 to collect granular click telemetry.
- Separate click analytics onto an asynchronous Kafka pipeline to prevent blocking redirect response latencies.
Topic Knowledge Assessment ๐ง
Step through 2 scenario questions to test your staff-level grasp.
What is the primary difference between returning HTTP 301 Moved Permanently vs HTTP 302 Found for a URL shortener redirect?
How clear and staff-actionable was this system breakdown?