A URL shortener is the most common warm-up in a system design interview because it hides real depth behind a tiny API. You take a long URL, return a short one like di.ai/aB3xZq, and redirect anyone who clicks it. The interesting parts are how you generate the short code, how you serve billions of redirects with low latency, and which tradeoffs you name out loud. Here is how we coach candidates to walk through it end to end, from clarifying scope to the read path that actually gets you hired.
Get the framing right first: this is a read-heavy system. Writes (creating a link) are rare; reads (following a link) dominate. Almost every design decision follows from that one fact, and the strongest candidates say it in the first two minutes.
Start with requirements and the math
Do not touch a whiteboard component until you have pinned down scope. The functional requirements are small: create a short URL from a long one, redirect from short to long, and optionally support custom aliases, expiration, and click analytics. Non-functional requirements are where the design lives: high availability, low redirect latency, and scale.
Anchor the scale to a real number. Bitly shortens 600 million links per month, so a realistic interview target is roughly 100 million new URLs per day. Then do the back-of-the-envelope estimate:
| Quantity | Estimate |
|---|---|
| New URLs/day | 100M |
| Writes/sec (avg) | ~1,160 |
| Read:write ratio | ~100:1 |
| Reads/sec (avg) | ~116,000 |
| 5-year storage (500 B/record) | ~90 TB |
The read:write ratio is the punchline. You are building a system that writes a little and reads enormously, which pushes you toward aggressive caching and a redirect path that touches as little as possible. Interviewers reward candidates who derive the caching decision from the numbers instead of bolting it on later. If estimation feels shaky, our practice sets let you rehearse this out loud with feedback.
Generate the short code
This is the core algorithmic decision, and there are three approaches worth knowing.
Hashing the long URL. Run the URL through MD5 or SHA-256, then take the first few characters. Simple, but you get collisions, and identical inputs always map to the same code (which leaks nothing useful and blocks per-user custom links). You end up adding collision-resolution logic that complicates the write path.
Counter plus base62. Maintain a global auto-incrementing ID and encode it in base62 using [a-z, A-Z, 0-9]. This is the approach popularized by Alex Xu's system design material and the one we recommend defaulting to. Base62 is just base conversion: 62^7 is about 3.5 trillion, so a 7-character code covers far more than the ~180 billion links you would generate in five years at this scale.
ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def encode(num: int) -> str:
if num == 0:
return ALPHABET[0]
s = []
base = len(ALPHABET) # 62
while num:
num, rem = divmod(num, base)
s.append(ALPHABET[rem])
return "".join(reversed(s))
The catch is the global counter. A single auto-increment column becomes a write bottleneck and a single point of failure. The standard fix is a token service that hands out ranges: each application server requests a block of IDs (say 1,000 at a time) from something like ZooKeeper or a dedicated counter service, then burns through them locally. This is worth mentioning explicitly, because "I'll use a counter" without addressing distribution is where junior candidates stall.
Pre-generated keys or random IDs. A key-generation service can pre-compute unique random base62 strings offline and dispense them on demand, which removes sequential predictability (base62 counters produce guessable, enumerable codes). If the interviewer raises enumeration or scraping concerns, this is your answer.
That base-conversion logic is also why firms like SIG, which lean heavily on tight algorithmic rounds, pair system design with coding problems that stress index math and in-place manipulation: think Rotate Image, Spiral Matrix, and the greedy line-packing in Text Justification. If that is your target, do not neglect the array and matrix fundamentals while you polish the design story.
Data model and the write path
The schema is deliberately boring:
url_mappings
short_code VARCHAR(7) PRIMARY KEY
long_url TEXT
created_at TIMESTAMP
expires_at TIMESTAMP NULL
user_id BIGINT NULL
You do not need joins, transactions across tables, or complex relations, so a relational database is fine but a key-value store (DynamoDB, Cassandra) fits the access pattern better: the entire workload is a point lookup by primary key. Partition or shard by short_code so reads spread evenly.
The write path is: validate the URL, grab an ID from your range allocator, encode to base62, persist the mapping, and return the short URL. For custom aliases, check uniqueness first and reject on conflict. A hash table is the right mental model for the whole store, which is why interviewers expect you to reason about collisions crisply even at the schema level.
Redirects: 301 vs 302
When a browser hits di.ai/aB3xZq, you look up the code and return a redirect. Which HTTP status you choose is a real design decision, not trivia.
A 301 status code indicates a permanent change in a URL's location, which means browsers and intermediaries cache it. That is great for latency: after the first hit, the client skips your server entirely on future visits. It is terrible for analytics, because you never see those subsequent clicks.
A 302 (or 307) is a temporary redirect. The 302, 303, and 307 status codes indicate that a resource is temporarily available under a new URL, meaning that the redirect has a limited life span and typically should not be cached. Every click comes back to your servers, so you can count it.
The tradeoff is the whole point: 301 minimizes load and latency; 302 preserves click tracking. Since analytics is how URL shorteners make money, most commercial services default to 302. State that reasoning and you have shown you understand the product, not just the protocol.
Scale the read-heavy workload
Now return to that 100:1 ratio and build the read path around it.
Cache aggressively. Put a distributed cache (Redis or Memcached) in front of the database and cache short_code -> long_url. The 20 percent of links that get 80 percent of traffic will live in memory, so most redirects never touch the database. Use LRU eviction; short codes are immutable, so cache invalidation is nearly free (entries only expire or age out).
Add a CDN and geo-distribution. Redirects are latency-sensitive because they sit in the critical path of every click. Serving them from edge locations close to users cuts round trips. Popular codes can be pushed to the CDN directly.
Separate read and write scaling. Because writes are ~1,000/sec and reads are ~100,000+/sec, scale them independently. Stateless application servers behind a load balancer handle both, but you provision read replicas and cache capacity far beyond write capacity.
Handle failure modes. Talk about a cache miss stampede on a viral link (use request coalescing or a short lock), database replica lag, and what happens when the ID allocator is unavailable (servers should have a buffered range to survive brief outages). Naming failure modes is what separates a mid-level answer from a senior one.
A clean version of the redirect flow:
GET /aB3xZq
-> check Redis (hit? return 302 immediately)
-> miss: read from KV store, populate cache
-> return 302 to long_url
-> async: increment click counter (queue/stream)
Note the analytics write is asynchronous. You never block a redirect on a metrics update; push the event to a queue (Kafka) and aggregate it downstream. That keeps p99 redirect latency low, which is the number the interviewer is really probing.
FAQ
How long should the short code be?
Seven base62 characters give 62^7, roughly 3.5 trillion combinations, which comfortably covers decades of growth at 100M links per day. Six characters (about 56 billion) works for smaller scale. Start with seven and explain the math; the reasoning matters more than the exact number.
Should I use a SQL or NoSQL database?
Either defends well. The workload is a point lookup by key with no relational logic, so a key-value store like DynamoDB or Cassandra matches the access pattern and scales horizontally with less effort. Pick one, state the reason (read pattern and shard-ability), and move on rather than debating both.
How do I prevent two servers from generating the same code?
Do not let servers independently pick codes. Use a central ID allocator that hands out non-overlapping ranges to each server, or a pre-generated key service that dispenses unique codes. Both eliminate collisions by construction, which is cleaner than hash-and-retry.
Is base62 encoding the only option?
No. Base62 is convention because it is URL-safe and human-typable. You could use base64 (but + and / need escaping) or pre-generated random keys. The important skill is explaining base conversion clearly, which overlaps with the math and bit-manipulation problems that show up in coding rounds.