requirements

A URL shortener — call it snip.link — takes a long URL and returns a short one that redirects to it. This is a classic first system design exercise precisely because it's small enough to fully design end to end in an hour, while still touching almost every building block in this track.
FunctionalNon-functional
Given a long URL, return a short codeRedirects must be very low latency (near-instant)
Visiting the short URL redirects to the originalRead-heavy: far more redirects happen than URLs get created
Optionally, let a user pick a custom short codeHigh availability — a broken shortener breaks every link that uses it, everywhere
Optionally, expire links after a set timeShort codes must not collide

estimation

Say snip.link expects 100 million new short URLs created per month, and a 100:1 read-to-write ratio (each link is visited roughly 100 times on average).

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
            
Read-heavy by two orders of magnitude, modest write volume, and a dataset that grows steadily but not explosively — these numbers directly justify the design choices below, particularly leaning hard on caching for reads.

API design


POST /urls          { "longUrl": "https://..." }  -> { "shortCode": "aZ9k2" }
GET  /{shortCode}   -> 301 redirect to the long URL
            
A 301 (permanent redirect) vs. 302 (temporary) is a real design choice: 301 lets browsers cache the redirect, reducing load on snip.link over time, but makes click analytics harder since the browser may skip re-requesting the shortener entirely on repeat visits. A 302 keeps every click hitting the server, which is worse for latency but better for tracking — a real trade-off worth naming explicitly rather than picking silently.

high-level design

ComponentRole
Load balancerDistributes both creation and redirect traffic across app servers — see Load Balancers.
App serversHandle URL creation (generate a short code, write to the database) and redirect lookups.
DatabaseStores 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.
CacheA distributed cache in front of the database absorbs the vast majority of redirect reads, given the 100:1 read-heavy ratio estimated above.

the interesting part: generating short codes without collisions

Three approaches, in increasing order of how well they scale:
ApproachHow it worksDownside
Random string + collision checkGenerate 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 URLTake 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 counterRun 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
            
This is the approach that scales cleanly: it pushes the actual hard problem (uniqueness at scale) onto the unique ID generation building block, which was already solved once, instead of re-solving it with collision-prone random strings.

evaluation

RequirementHow it's met
Low-latency redirectsCache absorbs almost all reads given the 100:1 ratio; cache misses fall back to a fast key-value lookup.
No collisionsBase62-encoded IDs from a uniqueness-guaranteed generator, not random strings.
High availabilityLoad-balanced, horizontally-scaled app servers and a replicated database/cache — no single point of failure.
Custom short codesHandled as a special case: check availability against the same database before falling back to the generated-ID path.

related topics

reference