Skip to main content

Command Palette

Search for a command to run...

Design a URL Shortener | System Design Question

Updated
10 min readView as Markdown

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.

More from this blog

P

Programming, System Design and AI | Latencot

53 posts

Here, you will read all about tech and tech updates. From writing code to building AI systems, we'll talk and share about everything.