AlgoInsight

System Design

Design a URL Shortener

It's the first system design question almost everyone gets - and the one most people fumble, because it looks too simple to take seriously. I've watched it separate hires from no-hires for years. Here's how to actually answer it.

By Admin2026-06-1815 min readurl-shortenersystem-designcase-studyinterviewscalabilitycaching

I've conducted more than three hundred interviews at Amazon, and "design a URL shortener" - design a service like bit.ly that turns a long link into a short one - is one I've used and seen used more than almost any other. Candidates relax when they hear it. Oh, this one's easy. That relief is exactly the trap. The question looks trivial, which is precisely why it's such a good filter: it reveals whether you can take a simple-sounding problem and reason about it rigorously, or whether you wave your hands because "it's just a lookup."

Here's how I'd want to see this answered - and, because I'm the one who's been on the other side of the table, where the points are actually won and lost.

Start by scoping, not designing

The single most common mistake I see: candidates start drawing boxes before they understand the problem. Strong candidates spend the first few minutes establishing what we're actually building. For a URL shortener, the functional requirements:

And optionally (ask the interviewer which matter): custom aliases, link expiration, and analytics on clicks.

Then the non-functional requirements - this is where the real design lives:

Interview: When I'm evaluating this question, the first thing I'm watching is whether you surface "this is read-heavy" yourself. That observation drives every good decision downstream - caching, replication, how you treat the database. Candidates who name it early and let it guide their design score well. Candidates who never mention it, and design as if reads and writes were balanced, have missed the defining characteristic of the system. State it out loud, early.

Estimate the scale

A quick back-of-the-envelope estimate shows you think about size before structure. Suppose we create 100 million new URLs per month. With a 100:1 read-to-write ratio, that's 10 billion redirects per month. Over a few years, we accumulate billions of stored URLs. These numbers tell us two things immediately: we need a lot of storage for the mappings, and the redirect path must handle very high read volume. Now we design for that, not for a toy.

The high-level design

At its core, the system is simpler than most: it stores a mapping from short code to long URL, and serves two operations against it.

graph TD
    subgraph CREATE[Create path]
        A[User submits long URL] --> B[App Server]
        B --> C[Generate unique short code]
        C --> D[(Store code → long URL)]
        D --> E[Return short URL]
    end
    subgraph REDIRECT[Redirect path - the hot path]
        F[User visits short URL] --> G[App Server]
        G --> H{In cache?}
        H -->|Yes| I[Return long URL, redirect]
        H -->|No| J[(Look up in database)]
        J --> K[Cache it, redirect]
    end
This diagram shows the two paths. On the create path, a user submits a long URL to an app server, which generates a unique short code, stores the mapping of code to long URL in the database, and returns the short URL. On the redirect path - the hot, high-traffic path - a user visits a short URL, the app server checks the cache first; on a hit it redirects immediately, and on a miss it looks up the database, caches the result, and redirects.</tts>

The redirect path is the hot path - it handles the vast majority of traffic - so it's where we focus optimization. Notice the cache sits right in front of the database on that path. Because the system is read-heavy and a small set of links are usually far more popular than the rest, caching is enormously effective here: most redirects can be served straight from cache without touching the database at all. (This is the read-heavy insight paying off - see the dedicated guide on caching for why this is the single highest-leverage move.)

The hard part: generating the short code

This is where the "easy" question gets interesting, and where I separate candidates. How do you generate a unique short code for each URL?

Approach 1 - Hash the URL. Run the long URL through a hash function and take the first few characters as the code. Simple, but it has a real problem: different URLs can hash to the same prefix (a collision), so you'd need to detect collisions and handle them, which adds complexity. It also means the same URL always produces the same code, which may or may not be desirable.

Approach 2 - Use a counter with base-62 encoding. Maintain a global counter that increments for each new URL, and encode that number in base 62 (using a-z, A-Z, 0-9 - 62 characters) to produce a short, unique code. Because each number is unique, each code is unique - no collisions to handle, ever. Seven base-62 characters give you over 3.5 trillion combinations, which is plenty for years of growth. This is clean and collision-free, and it's usually the answer I'm hoping to hear.

ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def encode(num):
    if num == 0:
        return ALPHABET[0]
    code = []
    while num > 0:
        num, rem = divmod(num, 62)
        code.append(ALPHABET[rem])
    return "".join(reversed(code))

# Each incrementing ID becomes a short, unique code
for id in [1, 1000, 1000000, 999999999999]:
    print(f"id {id:>13}  ->  {encode(id)}")

Run it: each unique number maps to a unique, compact code, and the codes stay short even for huge numbers. No collisions, by construction.

The catch - and the follow-up I always ask - is how do you generate that counter in a distributed system? A single counter on one machine is a bottleneck and a single point of failure. Strong candidates reach for a distributed approach: hand each application server a range of IDs to use (so they don't coordinate on every request), or use a distributed unique-ID generation scheme. This is exactly the distributed-unique-ID problem, and recognizing it - rather than assuming "just use a counter" works at scale - is a strong signal. (See the dedicated guide on generating unique IDs in distributed systems.)

Interview: The short-code question is the heart of this interview. Saying "I'll hash the URL" and stopping is a weak answer - it ignores collisions. Saying "I'll use a counter with base-62 encoding" is better. But the answer that earns top marks goes one step further, unprompted: "...and since a single counter doesn't scale, I'll distribute ID generation by handing each server a range." That move - anticipating the distributed bottleneck before I ask about it - is what separates a candidate who's memorized a design from one who actually understands it.

The database and caching

For storage, the access pattern is dead simple: given a short code, return the long URL. That's a key-value lookup, and it points toward a key-value store or a simple indexed table - nothing exotic. We're not doing complex queries or joins; we're doing an enormous number of simple lookups.

Because it's read-heavy, we scale the data layer the way read-heavy systems are scaled: caching in front (most redirects served from memory), and read replicas to spread the remaining read load. And since we accumulate billions of mappings over time, the storage may eventually need sharding across machines - partitioned by the short code, so a given code's lookup goes straight to the right shard. (These are the building blocks from the foundation - caching, replication, sharding - applied to a concrete problem.)

Concern Decision Why
Storage Key-value / indexed table Simple code → URL lookups, no joins
Hot path Cache in front Read-heavy; popular links served from memory
Read scaling Read replicas Spread the heavy read load
Data growth Shard by short code Billions of mappings over time
Availability Redundancy everywhere Downtime breaks every link

The redirect itself

One small but real detail interviewers like to probe: when a short URL is visited, the server responds with an HTTP redirect to the long URL. There's a choice between a permanent redirect (which browsers cache, reducing future load but making analytics harder because repeat visits may skip your server) and a temporary redirect (which always hits your server, enabling click tracking at the cost of more load). Mentioning this trade-off - and tying it to whether analytics matter - shows you're thinking about the system end to end, not just the storage. It's a small detail, but in my experience the candidates who surface it are the same ones who think carefully about every other decision in the design.

Deep dive: click analytics

If the interviewer asks for analytics - how many times each link was clicked, from where, when - this becomes an interesting sub-design rather than an afterthought. The naive approach is to increment a counter in the database on every redirect, but think about what that means: the redirect path is the hot path, handling enormous read volume, and now you're adding a write to the database on every single redirect. That write load would overwhelm the system and slow the very path you worked hardest to keep fast.

The better approach treats analytics as asynchronous. On each redirect, instead of synchronously updating a counter, you emit an event ("link X was clicked, from this location, at this time") to a message queue or event stream, and a separate analytics pipeline consumes those events and aggregates them in the background. This keeps the redirect path fast - it does the minimal work of redirecting and fires off an event - while analytics are computed out of band. This is the principle of moving slow work off the request path with a queue, applied to a concrete case. A candidate who recognizes that synchronous per-click writes would kill the hot path, and proposes async event processing instead, is showing real systems maturity.

Deep dive: custom aliases and expiration

Two common feature follow-ups, each with a small twist. Custom aliases - letting a user choose their own short code (like bit.ly/my-brand) - mean you can't rely purely on the counter; you now need to check whether the requested alias is already taken before accepting it, rejecting or suggesting alternatives if it is. It's a uniqueness check layered on top of the generation scheme. Expiration - links that stop working after a date - means storing an expiry timestamp with each mapping and checking it on redirect (returning an error for expired links), plus a background process to eventually clean up expired entries so they don't accumulate forever. Neither is hard, but handling them cleanly shows you can extend a core design to real requirements without breaking it.

Deep dive: abuse and safety

A follow-up that separates candidates who think about real systems: a URL shortener can be abused to hide malicious links (phishing, malware) behind innocent-looking short codes. A production design should consider this - checking submitted URLs against safe-browsing blocklists, rate-limiting how many links a single user can create to prevent spam, and being able to disable a malicious short code quickly. You wouldn't dwell here, but mentioning that a public URL shortener is a target for abuse, and that you'd build in basic safety checks and rate limiting, signals that you think about systems as they exist in the wild, not just on the happy path. (Rate limiting is its own building block - see the dedicated guide.)

One more latency consideration worth voicing: because the redirect sits in the critical path of someone clicking a link and waiting to land on a page, every millisecond counts. This is why the cache matters so much, why you'd place servers and caches close to users geographically, and why the lookup stays a single fast key-value operation. The whole system is, in a sense, an exercise in making one specific lookup as fast and as available as physically possible - and framing it that way shows you understand what the system is really optimizing for.

Bringing it together

So the complete picture: a create path that generates a unique code (counter plus base-62, with distributed ID generation) and stores the mapping; and a read-heavy redirect path fronted by a cache, backed by a replicated and eventually sharded key-value store, returning fast redirects. Simple at its core, but every decision justified by the read-heavy, high-availability nature of the problem.

Interview: What gets you hired on this question isn't a clever trick - it's disciplined reasoning about a deceptively simple problem. The signals I reward: scoping before designing, naming the read-heavy ratio and letting it drive decisions, handling short-code generation rigorously (including the distributed counter follow-up), and applying caching and replication for the right reasons. The candidates who struggle are the ones who treated "it's just a lookup" as license to skip the rigor. Take the simple question seriously, and it becomes one of the best opportunities to show how you think - which is exactly why interviewers keep reaching for it as an opener.

The bottom line

A URL shortener stores a mapping from short code to long URL and serves two operations: create and redirect. The design is driven by one defining fact - it's overwhelmingly read-heavy - which makes caching the highest-leverage decision and shapes how you scale the data layer. The genuinely interesting problem is generating unique short codes, where a counter with base-62 encoding beats hashing, and where the real depth is recognizing that the counter itself must be distributed to scale. The optional features - analytics, custom aliases, expiration, abuse prevention - each extend that core cleanly without disturbing the fast read path.

Treat the "easy" question with the rigor it deserves, justify every decision by the problem's nature, and you'll turn the system everyone underestimates into a showcase of how you actually think. We work through case studies like this end to end across System Design Fundamentals.


Go deeper

More articles