Backend
Designing a URL Shortener
The classic system design warm-up question, worked through end to end — encoding, collisions, caching, and the 301-vs-302 trade-off everyone forgets.
If you've done more than a handful of system design interviews, you've been asked this one. It's the "FizzBuzz" of system design — small enough to finish in thirty minutes, but with enough hidden depth that how you handle the follow-ups says more about you than the initial design does. The core requirement fits in a sentence: given a long URL, generate a short code, and store a mapping so that requesting the short code redirects to the original URL. Everything interesting happens in how you satisfy that sentence at scale.
Encoding approach #1: base62 an auto-incrementing ID
The simplest reliable approach sidesteps the "how do I generate a unique short string" problem entirely by not generating one directly. Instead, let a database (or a dedicated ID service) hand out auto-incrementing integers — 1, 2, 3, and so on — and encode each integer into a short string using base62: the ten digits, the 26 lowercase letters, and the 26 uppercase letters, giving 62 symbols per position instead of 10.
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
BASE = len(ALPHABET) # 62
def encode(id: int) -> str:
if id == 0:
return ALPHABET[0]
digits = []
while id > 0:
id, remainder = divmod(id, BASE)
digits.append(ALPHABET[remainder])
return "".join(reversed(digits))
# encode(125) -> "21"
# encode(56800235583) -> "zzzzzz" (six chars covers 62^6 ≈ 56.8B ids)Six base62 characters cover roughly 56.8 billion distinct ids, which is why almost every short-URL service you've ever used has codes in that length range. Because the underlying id is guaranteed unique by the database, the encoded code is unique for free — there is no collision to detect and no retry loop. The cost is a centralized point of coordination: something has to hand out the next id, and at very large write volumes that something becomes a bottleneck or a single point of failure. Production systems usually solve this the way Twitter's Snowflake does — pre-allocate blocks of ids to each server, or embed a timestamp and a worker id directly into the number — so no single component has to serialize every write.
The other cost is informational. A sequential id leaks the order and rough volume of link creation: if I shorten a URL today and get back a code that base62-decodes to 900,001, and I shorten another next week that decodes to 950,001, I now know roughly how many links your service created in between. For an internal tool that's harmless; for a competitor trying to estimate your traffic, it isn't.
Encoding approach #2: hash and truncate
The alternative is to derive the code from the URL itself instead of from a counter. Run the long URL through MD5 or SHA-256, and take the first six or seven characters of the resulting hex (or base62) digest as the short code. This removes the centralized counter entirely — any server can compute a code for any URL independently, with no coordination and no sequential leak.
What it introduces instead is collisions. Truncating a hash down to six or seven characters throws away most of its entropy, so two different long URLs will occasionally map to the same short code. That has to be handled explicitly: on a collision, either re-hash with a salt appended and try again, or store the full-length hash alongside the URL, detect the collision at write time, and only ever serve the short prefix once you've confirmed it's unique in your table. Either way, hashing trades "uniqueness by construction" for "uniqueness that must be verified," which is exactly the trade-off an interviewer wants you to name.
Reads dwarf writes
Whichever encoding you pick, the access pattern is lopsided: shortening a URL happens once, but the resulting short link might get clicked thousands of times. Reads (redirects) outnumber writes (shortenings) by orders of magnitude in virtually every real deployment.
Designing around reads, not writes
That read-heavy skew should drive most of the architecture. A cache sitting in front of the database — Redis or an equivalent, keyed by short code — should absorb the overwhelming majority of redirect traffic, so a "hot" link almost never touches the database after its first request. Read replicas soak up whatever does miss the cache. The write path, by contrast, can afford to be simpler and slower: it runs far less often, and a few extra milliseconds to allocate an id or check a hash for collisions is invisible to the user creating the link.
301 or 302? It matters more than it looks
The redirect itself has a decision hiding inside it. A 302 (temporary redirect) tells the browser "don't cache this, ask me again next time," which means every single click on the short link hits your service and you can log it — click counts, referrers, timestamps, geography, whatever your analytics need. A 301 (permanent redirect) tells the browser (and any CDN in front of it) "this mapping never changes, cache it yourself," which dramatically cuts the load on your servers but also means most future clicks never reach you at all — the browser just redirects locally from its own cache. If click analytics is a stated requirement, that alone should push you toward 302 despite the extra load, or toward a hybrid where you log asynchronously on a 302 for a window of time before flipping older, cold links to 301.
Natural extensions to expect as follow-ups
Two features come up almost every time. Custom aliases — letting a user pick go/launch instead of accepting a generated code — sound like a small UI addition but actually reopen the uniqueness problem you just solved: a chosen alias has to be checked against every existing code before it's accepted, which the base62-counter approach conveniently avoided. Expiration is the other one: adding a TTL to a link means a background job (or lazy check on read) that evicts expired codes from the cache and, eventually, from storage, freeing up short codes for reuse if your namespace is tight enough to need it.
Key takeaways
- The whole problem reduces to one sentence: map a short code to a long URL, uniquely and quickly.
- Base62-encoding an auto-incrementing id gives uniqueness for free but needs a centralized (or Snowflake-style distributed) id generator and leaks creation volume.
- Hashing the URL and truncating removes the centralized counter but reintroduces collisions that must be explicitly detected and retried.
- Redirects vastly outnumber shortenings — design the read path around caching and replicas, and let the write path stay simple.
- 302 preserves click analytics by keeping every redirect server-side; 301 offloads traffic to browser/CDN caches at the cost of that visibility.
- Custom aliases and link expiration are the two follow-ups most likely to come up, and both add real complexity to the base design.
