How would you design a URL shortener service?
The core of the system is a mapping from a short code to a long URL, plus two very different access patterns: writes (creating a short link) are rare, reads (someone clicking a short link and getting redirected) are extremely frequent. Design for the read path first, since that’s where the real load lives.
Encoding the short code
Don’t hash the long URL, hashes collide and you’d need collision handling anyway. Instead, use a counter (an auto-incrementing ID from a database sequence or a distributed ID generator) and encode that number in base62 (a-z, A-Z, 0-9), which gives a short, unique, collision-free code with no extra lookup needed. A 6-character base62 code covers over 56 billion unique values, which is enough headroom for most systems for years.
Storage and the read path
Store the mapping in a key-value store or a simple relational table indexed on the short code. Since redirects are read-heavy and latency-sensitive (a slow redirect is a bad user experience), put a cache (Redis or a CDN edge cache) in front of the database for the hot path, most systems see a small fraction of links account for the majority of clicks, so caching the popular ones has an outsized effect.
Best practice
Use a 301 (permanent) or 302 (temporary) redirect deliberately, not interchangeably: 301 lets browsers cache the redirect and skip your server on repeat visits, which is great for load but means you lose the ability to change or track that specific redirect later; 302 keeps every click hitting your server, which costs more but keeps click analytics and the ability to update the destination fully accurate.
Edge case interviewers probe for
Ask how you’d handle custom aliases (a user picking their own short code instead of an auto-generated one). This needs a uniqueness check against existing codes before insert, and since custom codes can collide with future auto-generated ones, you either reserve a separate character-length range for custom codes or check both spaces on every generation.
Common mistake
Proposing a hash-based approach (like taking the first 6 characters of an MD5 hash of the URL) without a clear collision-handling strategy, or forgetting that the same long URL requested twice should either return the same short code or explicitly support multiple short codes per URL, a design decision worth stating explicitly rather than leaving implicit.
What the interviewer is checking
Whether you correctly identify the read-heavy access pattern and design the caching and redirect strategy around it, rather than treating this as a simple CRUD problem.
Think of a coat check at a busy theater. When you drop off your coat, you get a small numbered ticket, that’s the short code. The coat check doesn’t need to look at your coat’s color or brand to give you a number, it just hands out the next number in sequence. Getting your coat back (the redirect) just means showing that ticket, fast, no searching required, because the number directly points to the right hook.
Encoding the number in base62 instead of writing “coat number 48,203,917” on the ticket is like using a mix of letters and numbers instead of only digits, so the same amount of information fits on a much smaller ticket. And putting a cache in front is like keeping the 20 most-requested coats near the front counter instead of buried in the back room, since a handful of coats (or links) get picked up way more often than the rest.
Why interviewers ask this
It’s a compact system design problem that still surfaces real tradeoffs: encoding strategy, read/write asymmetry, caching, and redirect semantics, in about 30 minutes.
What a strong answer signals
That you design for the actual traffic pattern (read-heavy) instead of treating every operation as equally important.
Common follow-ups
- How would you shard the database as it grows?
- How would you expire or delete old links?
- How would you add click analytics without slowing down redirects?
Advanced variation
“How would you generate unique IDs across multiple servers without a single counter becoming a bottleneck,” which expects knowledge of distributed ID generation approaches like Snowflake IDs.
A marketing team’s link-shortening tool initially stored click analytics synchronously on every redirect request, writing directly to the main database before returning the 302 response, which added noticeable latency under load. Moving analytics writes to an asynchronous queue (fire off the click event, redirect immediately, process the analytics write separately) cut median redirect latency significantly, since the user-facing redirect no longer waited on a database write that had nothing to do with getting them to the right destination.
BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(num: int) -> str:
# Converts an auto-incrementing DB id into a short, collision-free code.
if num == 0:
return BASE62[0]
chars = []
while num > 0:
num, rem = divmod(num, 62)
chars.append(BASE62[rem])
return "".join(reversed(chars))
def create_short_link(db, long_url: str) -> str:
row_id = db.insert_and_get_id("urls", {"long_url": long_url})
return encode_base62(row_id) # e.g. "b7X"- 1Design for the read path first: redirects vastly outnumber link creations in real usage.
- 2Encode an incrementing counter in base62 instead of hashing the URL, avoiding collision handling entirely.
- 3Cache popular short codes since a small fraction of links usually account for most clicks.
- 4Choose 301 vs 302 redirects deliberately based on whether you need ongoing analytics or want browser-side caching.
- 5Move non-critical work like click analytics off the synchronous redirect path to keep it fast.