Designing a URL Shortener
A small enough problem to fully design end to end — and one that touches caching, key-value storage, and unique ID generation all at once.
Intermediate
| Functional | Non-functional |
|---|---|
| Given a long URL, return a short code | Redirects must be very low latency (near-instant) |
| Visiting the short URL redirects to the original | Read-heavy: far more redirects happen than URLs get created |
| Optionally, let a user pick a custom short code | High availability — a broken shortener breaks every link that uses it, everywhere |
| Optionally, expire links after a set time | Short codes must not collide |
Writes/sec = 100,000,000 / (30 × 86,400) ≈ 39 writes/sec
Reads/sec = 39 × 100 ≈ 3,900 reads/sec
Storage/URL ≈ 500 bytes (long URL + metadata)
Storage/yr = 100M × 12 × 500 bytes ≈ 600 GB/year
POST /urls { "longUrl": "https://..." } -> { "shortCode": "aZ9k2" }
GET /{shortCode} -> 301 redirect to the long URL
| Component | Role |
|---|---|
| Load balancer | Distributes both creation and redirect traffic across app servers — see Load Balancers. |
| App servers | Handle URL creation (generate a short code, write to the database) and redirect lookups. |
| Database | Stores the long URL, short code, and metadata (creation time, expiry, click count). A key-value store is a strong fit here — see Key-Value Stores & the Dynamo Model — since access is always by exact short code, never by a complex query. |
| Cache | A distributed cache in front of the database absorbs the vast majority of redirect reads, given the 100:1 read-heavy ratio estimated above. |
| Approach | How it works | Downside |
|---|---|---|
| Random string + collision check | Generate a random 7-character string, check if it's taken, retry on collision. | Collision rate rises as the namespace fills, and every write pays for a read-before-write check. |
| Hash the long URL | Take a hash (e.g. MD5) of the long URL, use the first 7 characters. | Two different long URLs can hash to the same prefix; still needs a collision check. |
| Base62-encode a unique counter | Run URLs through a distributed unique ID generator, then base62-encode that ID into a short string. | No collisions by construction — the ID generator already guarantees uniqueness, so the string is just a compact re-encoding of a number that was already unique. |
id = 125_000_000 (from a Snowflake-style or range-handler ID generator)
base62(125_000_000) = "8M0kX" # a short, unique, non-colliding code
| Requirement | How it's met |
|---|---|
| Low-latency redirects | Cache absorbs almost all reads given the 100:1 ratio; cache misses fall back to a fast key-value lookup. |
| No collisions | Base62-encoded IDs from a uniqueness-guaranteed generator, not random strings. |
| High availability | Load-balanced, horizontally-scaled app servers and a replicated database/cache — no single point of failure. |
| Custom short codes | Handled as a special case: check availability against the same database before falling back to the generated-ID path. |