System Design
Design: URL Shortener
Problem Statement
Design a service like bit.ly: given a long URL, return a short URL that redirects to it when visited.
Requirements
Functional
- Given a long URL, generate a unique short URL.
- Visiting the short URL redirects (HTTP 301/302) to the original long URL.
- Optionally: let users pick a custom alias, and support link expiration.
Non-functional
- Read-heavy: redirects vastly outnumber creations (often 100:1 or more).
- Redirects should be low-latency (a redirect is on the critical path of someone’s click).
- Short URLs must not collide — two different long URLs can never map to the same short code.
- Should scale to hundreds of millions of URLs.
High-Level Design
At a high level, there are two endpoints:
POST /shorten— takes a long URL, returns a short code.GET /{code}— looks up the long URL forcodeand redirects to it.
Components:
- API layer — stateless, handles both endpoints, sits behind a load balancer so it can scale horizontally.
- Key generation service — produces unique short codes.
- Database — stores the mapping from short code → long URL (plus metadata like creation time and expiration).
- Cache (e.g. Redis) — sits in front of the database for the read path, since redirects are the hot path and the mapping is immutable once created (cache-friendly).
client → API layer → cache (hit? return) → DB (miss → populate cache) → redirect
Deep Dive
Generating the short code. Two common approaches:
- Base62 encode an auto-incrementing ID. A counter (e.g. from the DB
or a dedicated ID-generation service like Snowflake) hands out unique
integers; each integer is base62-encoded (
[a-zA-Z0-9]) into a short string. Simple, guaranteed unique, and a 7-character base62 string covers 62^7 ≈ 3.5 trillion codes — plenty of headroom. - Hash the long URL (e.g. MD5/SHA-256) and take the first N characters. Simpler conceptually, but needs collision handling (two different URLs can hash to the same prefix), which adds complexity back in. The counter-based approach avoids this entirely, so it’s usually the better default.
Data model is intentionally minimal:
| Column | Type | Notes |
|---|---|---|
code |
string PK | the short code |
long_url |
text | the original URL |
created_at |
timestamp | |
expires_at |
timestamp | nullable |
Why the cache matters: once a (code → long_url) mapping is written,
it never changes. That makes it ideal for caching — a cache-aside strategy
(check cache, fall back to DB on miss, populate cache) absorbs the vast
majority of read traffic and keeps the database load light even at high
QPS.
Trade-offs & Alternatives
- 301 vs 302 redirect: a 301 (permanent) lets browsers cache the redirect, reducing load on your service — but it also means you lose visibility into every subsequent click, which matters if you want click analytics. A 302 (temporary) keeps every click hitting your service. Most link shorteners that care about analytics use 302 despite the extra load.
- Counter-based IDs need coordination in a distributed setup — a single auto-incrementing counter becomes a bottleneck/single point of failure at scale. A common fix is a Snowflake-style ID generator (or pre-allocating ID ranges to each server) so no single component has to serialize every ID request.
- At 10x the scale, the next bottleneck is usually the database itself — sharding by short code (e.g. by a hash of the code) becomes necessary once a single DB instance can’t hold the full mapping table or serve the write throughput.
Wrapping Up
The core of this design is simple — a key-value mapping with a cache in front of it — and most of the interesting decisions are about how you generate unique keys and how you keep the hot read path fast as traffic grows.