Design a URL Shortener (TinyURL)
The canonical system design interview problem: back-of-the-envelope capacity estimations for 100M URLs/day, Base62 encoding vs. Key Generation Services (KGS), 301 vs. 302 HTTP redirect mechanics, and low-latency Redis caching.
1. Requirements & Back-of-the-Envelope Estimation
Let's establish the core scope and system scale for a planetary-level URL shortening service like Bitly or TinyURL:
Functional Requirements
- Given a long URL, return a short unique alias (e.g.
https://tiny.url/7bX9q1). - Redirect short links to the original long URL with sub-10ms latency.
- Allow users to specify optional custom aliases (e.g.
/my-cool-link). - Links can optionally expire after a configurable TTL.
Non-Functional Requirements
- High Availability (99.99%): Redirections must never go down.
- Ultra-Low Latency: P99 redirect latency < 10ms.
- Read-Heavy: 10:1 Read-to-Write ratio.
- Non-Predictable: Short IDs should not be sequentially guessable.
2. 301 Permanent vs. 302/307 Temporary Redirects
When a user visits https://tiny.url/7bX9q1, which HTTP status code should the server return? This is a classic interview trade-off question:
HTTP 301 Moved Permanently
The browser caches the redirection permanently on the user's local device. Subsequent visits bypass our backend entirely!
HTTP 302 / 307 Temporary Redirect
The browser sends every single request to our server, and we respond with Location: https://target.com.
3. Base62 Encoding & The Key Generation Service (KGS)
Why Base62? Base62 uses [0-9, a-z, A-Z] (62 characters total), which is URL-safe and avoids special characters like + or / found in Base64:
Method 1: MD5 / SHA-256 Hash + Truncate
Compute MD5(long_url) and take first 7 characters.
Method 2: 64-bit Unique ID → Base62 (Gold Standard)
Generate a 64-bit unique integer (via Snowflake or ZooKeeper token range), then convert the integer directly to Base62.
4. End-to-End System Architecture
5. Interactive Base62 & URL Shortener Lab
Type a long URL or unique 64-bit integer. Observe real-time Base62 conversion, simulated Redis cache hits, and 301 vs 302 redirection telemetry.
• HTTP Status: 302 Found
• Analytics: Click event dispatched to Kafka
6. Base62 Shortening Service in 5 Languages
class Base62Codec:
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
BASE = len(ALPHABET)
@classmethod
def encode(cls, num: int) -> str:
if num == 0:
return cls.ALPHABET[0]
digits = []
while num > 0:
digits.append(cls.ALPHABET[num % cls.BASE])
num //= cls.BASE
return ''.join(reversed(digits))
@classmethod
def decode(cls, s: str) -> int:
num = 0
for char in s:
num = num * cls.BASE + cls.ALPHABET.index(char)
return num
class UrlShortenerService:
def __init__(self, id_generator, redis_client, db_client):
self.id_gen = id_generator
self.redis = redis_client
self.db = db_client
def shorten_url(self, long_url: str) -> str:
# 1. Get globally unique 64-bit ID
unique_id = self.id_gen.generate_id()
# 2. Encode to Base62
short_code = Base62Codec.encode(unique_id)
# 3. Store mapping in DB and warm Redis cache
self.db.save(short_code, long_url)
self.redis.set(f"url:{short_code}", long_url, ex=86400)
return f"https://tiny.url/{short_code}"
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE = BigInt(62);
export class Base62Codec {
static encode(id: bigint): string {
if (id === 0n) return ALPHABET[0];
let num = id;
let result = "";
while (num > 0n) {
result = ALPHABET[Number(num % BASE)] + result;
num = num / BASE;
}
return result;
}
static decode(str: string): bigint {
let num = 0n;
for (const char of str) {
num = num * BASE + BigInt(ALPHABET.indexOf(char));
}
return num;
}
}
package main
import (
"strings"
)
const Alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const Base = uint64(len(Alphabet))
func EncodeBase62(num uint64) string {
if num == 0 {
return string(Alphabet[0])
}
var sb strings.Builder
for num > 0 {
rem := num % Base
sb.WriteByte(Alphabet[rem])
num /= Base
}
// Reverse string
runes := []rune(sb.String())
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
public class Base62Codec {
private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int BASE = ALPHABET.length();
public static String encode(long num) {
if (num == 0) return String.valueOf(ALPHABET.charAt(0));
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(ALPHABET.charAt((int)(num % BASE)));
num /= BASE;
}
return sb.reverse().toString();
}
public static long decode(String str) {
long num = 0;
for (char c : str.toCharArray()) {
num = num * BASE + ALPHABET.indexOf(c);
}
return num;
}
}
#include <string>
#include <algorithm>
const std::string ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::string encode_base62(uint64_t num) {
if (num == 0) return std::string(1, ALPHABET[0]);
std::string result = "";
while (num > 0) {
result += ALPHABET[num % 62];
num /= 62;
}
std::reverse(result.begin(), result.end());
return result;
}
7. URL Shortener Scenario Quizzes
Scenario 1: A marketing team demands that short URLs return HTTP 301 Permanent Redirects to reduce server load. Why would the analytics team strongly object to this?
Scenario 2: Two concurrent users attempt to claim the same custom short alias 'my-brand' at the exact same millisecond. How should the database layer handle this race condition?
Ready to Build? Draw & AI-Validate the TinyURL Architecture
You've mastered the 100M writes/day capacity math, Base62 token ranges, 301 vs 302 redirect mechanics, and Redis cache stampede mitigations. Put your knowledge to the test on the interactive canvas and get instant AI grading!