A URL shortener is the classic "sounds trivial, actually teaches you a lot" system. You take a long URL, hand back a short one, and when someone visits the short link you send them to the original. You could build a working version in an afternoon with a database table and two routes. The reason it shows up in interviews is that the moment you care about scale, short codes, and speed, every simple choice turns into a real decision.
Let me start with the shape of the workload, because it drives everything. This system is read heavy and write light. You create a short link once. It might then get clicked thousands or millions of times. So the redirect path has to be blisteringly fast and cheap, while the creation path can afford to do a bit more work. Any design that treats reads and writes as equally important is optimizing the wrong thing.
The core question is how you generate the short code. You have two main families of approaches, and the trade-off between them is the heart of the design.
The first approach is hashing. Take the long URL, run it through a hash, and use some characters of the output as the code. It is simple and stateless. The problem is collisions. Two different URLs can hash to the same short prefix, and now you have to detect that and retry with a different slice, which means a database check on every creation and awkward edge cases. It works, but it gets fiddly.
The second approach, which I prefer, is to use a counter. You keep a global, ever-increasing number, and for each new URL you take the next value and encode it. The trick is base62 encoding. Instead of writing the number in base 10, you write it using all lowercase letters, uppercase letters, and digits, which gives you 62 symbols per character. That is dense. Six base62 characters give you around 56 billion combinations, and seven give you trillions. So a short, clean code covers you for a very long time. Because each number is unique, the codes are unique by construction. There are no collisions to check, which makes creation fast and predictable.
The catch with a single counter is obvious: it is a single point of contention and failure. If every write in the world has to grab the next value from one place, that place becomes your bottleneck. The standard fix is to hand out ranges. Each application server requests a block of the number space, say a range of a thousand or a million IDs, and serves codes from its local block without talking to the central allocator again until the block runs out. Now the counter is touched rarely, servers mint codes independently, and you have removed the hot spot. You give up perfectly sequential codes, since blocks interleave, but nobody cares whether short codes are strictly in order.
Now the redirect path, which is where the traffic actually lives. A visit to a short link needs to turn a code into a long URL and issue a redirect, ideally in single digit milliseconds. The lookup itself is a primary key read on the code, which is already fast, but at high volume you do not want to hit the database for every click. So you put a cache in front. Short codes and their destinations are small, they almost never change, and popular links get hit over and over, which is close to the ideal cache workload. A large fraction of redirects should be served straight from memory and never touch the database at all.
One more decision people gloss over: which HTTP redirect to use. A 301 permanent redirect lets browsers and proxies cache the mapping, which reduces load on you, but it also means you stop seeing many of the clicks, which kills your analytics. A 302 temporary redirect sends every click through your server, so you can count it, at the cost of more traffic. If you care about click tracking, and most of these products do, you take the 302 and the extra load on purpose.
How do the real ones do it? Services in this space generally use counter based or key generation approaches rather than fragile hashing, base62 style encoding for compact codes, aggressive caching on the read path, and they lean into the read heavy nature of the workload. The analytics side, counting clicks and enriching them, is often a bigger system than the shortener itself.
The reason this problem is a good teacher is that it forces you to separate a read path from a write path and to think hard about generating unique identifiers without a bottleneck. Those two ideas show up in half the systems you will ever build.
I wrote the full breakdown, with diagrams and the data model, here: https://www.systemdesign.academy/interview/design-tiny-url-shortener
Top comments (0)