Skip to main content

Design a URL Shortener | System Design Question

Updated 10 min readMarkdown
Part of seriesSystem Design Guide

Time to put the framework to work on a real problem. The URL shortener - TinyURL, Bitly - is the classic first system design question because it is small enough to finish in an hour yet exercises every step. We will walk it end to end, starting the way a real interview does: with the questions you ask and the answers you would get back, before a single box is drawn.

Step 1: Ask questions, and be ready for messy answers

The prompt "design a URL shortener" is vague on purpose, and here is the part the polished write-ups skip: you will not always get clean answers. A strong interviewer hands you numbers; a distracted or inexperienced one says "you decide," gives a fuzzy reply, or bolts on a surprise feature halfway through. Your job is to ask anyway and keep the problem moving no matter what comes back. A realistic exchange, punts and all:

You: Quick example - what does the service actually do? Interviewer: A long URL comes in, you return a short alias, and clicking the alias redirects to the original.

You: Roughly what scale - creates per day, or per second? Interviewer: Eh, you tell me.

That "you tell me" is where people freeze. Don't - propose a number, say it out loud, and write it down: "I'll assume 100 million new URLs a day, about 1,000 writes per second, and design for that - stop me if you had a very different scale in mind." Now you own the scale instead of waiting for it. Scale is the one thing you cannot leave unset: "5 GB once a day" and "5 PB per second" are completely different systems.

You: How short should the code be, and which characters can it use? Interviewer: As short as possible. Alphanumeric is fine.

Alphanumeric means 62 characters (base-62) - and it quietly rules out base64, whose + and / are not URL-safe. "As short as possible" you will turn into a concrete length during the estimate.

You: If two people shorten the same long URL, same code or different? Interviewer: Same code - dedupe them.

That one decision shapes your write path: dedupe means a lookup on every create. Had they said "different every time," you would skip that lookup entirely.

You: Anything beyond shorten and redirect - custom aliases, expiry, analytics? Interviewer: Actually, yeah - let's support custom aliases and click counts.

There is the curveball. A less experienced interviewer will cheerfully pile on scope, so keep the one-line cost of each ready: a custom alias adds a uniqueness check on a user-supplied code; click analytics adds an async events pipeline and nudges you toward a 302 redirect (which routes every click through your service) over a 301. You do not design them now - you name the cost and park them: "Got it - alias needs a collision check on user input, analytics needs an events pipeline; I'll get the core working first and fold those in if we have time."

The rule underneath all of it: when the interviewer answers, use it; when they punt, make a reasonable assumption and state it; when they add scope, name its cost and keep going. Never stall waiting for a tidy answer that may not come.

With that settled, write the scope as precise points - not prose:

Functional requirements

  • Create a short URL from a given long URL.
  • Redirect a short URL to the original long URL.
  • Short codes are alphanumeric: [0-9, a-z, A-Z].
  • The same long URL maps to one short code (dedupe on create).
  • Links are immutable - no edit, no delete.
  • Custom aliases and click analytics: acknowledged, deferred.

Non-functional requirements

  • Read-heavy: redirects outnumber creates roughly 10:1.
  • Low redirect latency (target under ~100 ms).
  • High availability - the redirect path is critical (~99.9%+).
  • Short codes are unique and not trivially guessable in bulk.
  • Storage scales to hundreds of billions of records.

A quick estimate turns the scale into numbers. From the 100 million a day you assumed:

Write QPS = 100,000,000 / 100,000 (seconds/day) = ~1,000/s
Read QPS  = 1,000 x 10                            = ~10,000/s
Records in 10 years = 100M x 365 x 10             = ~365 billion

Two facts fall out immediately and shape everything: the system is heavily read-heavy (so cache the redirect path), and it must store hundreds of billions of records (so the store has to scale). Write throughput at ~1,000/s is modest.

Step 2: High-level design

The API is just two REST endpoints:

POST /api/v1/data/shorten   body: { longUrl }        -> returns shortUrl
GET  /{shortUrl}            -> HTTP redirect to longUrl

The architecture is a stateless web tier behind a load balancer, a database for the mappings, a cache in front of reads, and a unique-ID generator that the shortening path uses (more on that in the deep dive).

High-level architecture of a URL shortener: client to load balancer to stateless web servers, backed by a Redis cache, a URL database, and a unique ID generator

The data model is trivial - a single table of mappings, so a relational database is a fine starting point:

url ( id BIGINT PRIMARY KEY, short_url VARCHAR, long_url VARCHAR )

301 vs 302 redirect is the one API subtlety worth raising. A 301 (permanent) tells the browser to cache the redirect, so future clicks skip your service entirely and go straight to the long URL - great for reducing server load. A 302 (temporary) routes every click through your service first - worse for load, but it lets you track click analytics. Pick 301 to offload traffic, 302 if analytics matter.

Step 3: Deep dive - generating the short code

This is the heart of the problem and where the interviewer wants depth: how do you turn a long URL into a unique 7-character code?

Why 7 characters? With 62 possible characters per position, a code of length n gives 62^n combinations. We need to cover 365 billion URLs, and 62^7 ≈ 3.5 trillion, comfortably more than enough - so 7 characters it is.

There are two standard approaches.

Option A - hash + collision resolution. Hash the long URL (CRC32, MD5, SHA-1) and take the first 7 characters. Simple, but two different URLs can collide on the same 7 chars. You resolve collisions by appending a predefined string and re-hashing until the code is free - which means a database check on every shorten, and a Bloom filter to make "does this code exist?" cheap.

Option B - base-62 conversion (the cleaner choice). Generate a unique numeric ID, then convert that number to base 62. Because each ID is unique, the resulting code is unique by construction - no collisions, no lookups to check.

Base-62 conversion: a unique numeric ID like 2009215674938 is encoded using the 62 characters 0-9 a-z A-Z into the short code zn9edcu, giving short.ly/zn9edcu

The catch base-62 pushes onto you: you now need globally unique IDs in a distributed system. That is its own well-known sub-problem (a Snowflake-style distributed ID generator, or a database auto-increment for smaller scale) - worth naming so the interviewer knows you see it.

The shortening (write) flow ties it together, with a dedup check so the same long URL is not stored twice:

Shorten flow: client POSTs a long URL; the server checks the database for an existing mapping and returns it if present; otherwise it gets a new unique ID, base-62 encodes it, stores the mapping, and returns the short URL

The redirect (read) flow is where the read-heavy requirement pays off. Because redirects dominate 10:1, we put a cache in front of the database: check the cache first, fall back to the database on a miss, and populate the cache for next time. Most redirects never touch the database at all.

Redirect flow: client requests a short code; the load balancer forwards to a web server, which checks Redis first and returns the long URL on a hit, or falls back to the database on a miss (repopulating the cache), then issues a 301 redirect

Step 4: Wrap up

With the core designed, name the improvements you would make with more time - this is where you show you see past the happy path:

  • Rate limiting. A malicious client could flood the shorten endpoint; throttle by IP or API key.
  • Scale the web tier. It is stateless, so scaling is just adding servers behind the load balancer.
  • Scale the database. ~365 billion rows means replication for read capacity and sharding (by the short code) for storage.
  • Analytics. Click counts, referrers, and geography are a common follow-on - and the reason you might choose 302 over 301.
  • Availability and consistency. Since a stale-but-available redirect is fine, this system leans toward availability; the mapping is immutable once written, which makes caching and replication easy.

Follow-up questions to expect

Once the core is on the board, the interviewer starts probing - this is where a lot of the signal is. Have crisp, reasoned answers ready:

  • "What if the unique ID generator dies?" It is the one component the write path cannot do without, so I would not run it as a single node. A Snowflake-style generator gives each node its own machine id, so several run independently with no coordination, and I could hand each app server a pre-allocated id range as a fallback. Redirects never touch it, so reads stay up regardless.
  • "A link goes viral - millions of hits a minute. What breaks?" Nothing new: that code is already hot in the cache, so it serves from memory. I would add cache replicas for headroom and, because the mapping is immutable, push it to a CDN or edge cache so the redirect resolves near the user without ever reaching our servers.
  • "How would custom aliases work?" The user supplies the code, so I need a uniqueness check before saving - a conditional write against the same key space as generated codes - and I reject it and ask for another if it is already taken.
  • "How do links expire?" Add an expires_at field, set a matching TTL on the cache entry, and let a background job sweep expired rows; a redirect past expiry returns 410 Gone.
  • "Are the short codes guessable?" With sequential ids, base-62 codes are enumerable - someone could walk the whole space and scrape every link. If that matters, I would randomize or hash the id before encoding, trading a character or two of length for unpredictability.
  • "Cache and database consistency?" This design gets lucky here: a mapping never changes once written, so the cache cannot go stale - there is no invalidation problem, only a cold-cache miss that loads the value once.
  • "How do you scale this to 10x?" Almost nothing structural changes, which is the whole point: the stateless web tier adds instances, the cache adds replicas, the database shards further by short code, and the ID generator adds nodes.

What you just built

A URL shortener is the ideal first walkthrough because the shape generalizes: scope and estimate, sketch a stateless tier with a cache and a database, then deep-dive the one hard part - here, unique short-code generation via base-62 over a distributed ID. Reads dominate, so caching carries the load; writes are simple; storage is the thing that has to scale.

Next in the series we take on a read-heavy problem where the deep dive is harder - a news feed - and the fanout question from earlier posts comes back in full. The framework followed here is from Alex Xu's System Design Interview.

No comments yet. Be the first to comment.

More from this blog