System Design · step by stepDesign a URL Shortener
Step 1 / 9
▶  Watch it explained

Prefer a video walkthrough?

Design a URL Shortener — the walkthrough in full

A written version of the interactive walkthrough above — the same steps, decisions and trade-offs, laid out for reading, reference and search.

The big idea

What is a URL shortener?

Strip away the brand and a URL shortener does one tiny thing: take a long, ugly link and hand back a short one — then, when anyone taps the short one, instantly send them to the original.

Sounds trivial. But "instantly", "anyone", and "never collide" turn it into a real systems problem: how do we mint a unique code for every link, and resolve billions of clicks in single-digit milliseconds without ever sending someone to the wrong page?

How to read this: Each step opens with a real design decision — you make the call before I show you what ships. Watch the diagram on the right grow, hover the boxes, and at the end kill the cache to see what breaks. Hit Begin.

Step 1 · The skeleton

Two jobs, one server

There are really only two operations: shorten (give me a long URL, return a short code) and redirect (given a code, send me to the long URL). Both need somewhere to keep the mapping — but where?

Design decision: Two jobs: shorten (long URL → code) and redirect (code → long URL). What’s the minimal structure?

The call: A server in the middle with a database behind it. — To shorten, the server saves code → long_url; to redirect, it looks the code up and returns a 301/302. The classic client → server → database spine: client asks, server decides, database never forgets.

Put a server in the middle and a database behind it. To shorten, the server saves a row code → long_url. To redirect, it looks the code up and answers with an HTTP 301/302 to the original. This is the client → server → database spine under every app.

Client–server model: The browser is the client (it asks). The server is the brain (it decides). The database is memory (it never forgets the mapping). Keep these three roles separate and everything else gets easier.

Step 2 · The short code

How do we mint the code?

Every link needs a unique code like /aZ9k2x. Hashing the URL gives collisions and runs long; pure random risks clashes and forces an "is this taken?" check on every single write. How do you guarantee uniqueness cheaply?

Design decision: Every link needs a unique short code like /aZ9k2x. How do you mint it?

The call: A counter encoded in base62, handed out by a Key Gen Service. — Counter 125 → "cb"; the KGS ensures two servers never mint the same number, so codes are unique by construction — no collision check. 62⁷ ≈ 3.5 trillion codes.

Treat it as a number. Keep a global counter and encode it in base62 (a–z A–Z 0–9): counter 125 becomes "cb". A dedicated Key Generation Service hands out ids — so codes are unique by construction, no collision check needed.

Base62, briefly: 62 symbols across 7 slots gives 62⁷ ≈ 3.5 trillion codes — enough to never run out. Counting up guarantees uniqueness; the KGS just makes sure two servers never hand out the same number.

Step 3 · The hot path

Reads dwarf writes

People create a link once but click it thousands of times. The redirect path runs maybe 100× more than the write path — and every redirect that hits the disk database adds latency to someone’s tap. How do you keep it sub-millisecond?

Design decision: A link is created once but clicked thousands of times. How do you keep redirects sub-millisecond?

The call: Cache hot code→url mappings in Redis, in front of the DB. — A hit returns in <1ms; a miss falls through to the store and warms the cache. Because a mapping never changes, it’s perfectly cacheable — no invalidation, pure upside.

Put a cache (Redis) in front of the database. A redirect checks the cache first; on a hit it returns in under a millisecond. On a miss it falls through to the database, then warms the cache. Because a mapping never changes, it’s perfectly cacheable.

301 vs 302: A 301 (permanent) lets browsers cache the redirect and skip you next time — great for load, useless for counting clicks. A 302 (temporary) routes every click through you so you can track it. Pick your trade.

Step 4 · Don’t fall over

Now serve the whole internet

One server was fine for a demo. Under real traffic it melts, and if it dies the whole service goes dark. Reads and writes also have wildly different shapes — mixing them on one box wastes resources. How do you scale?

Design decision: One server melts under real traffic, and reads outnumber writes ~100:1. How do you scale?

The call: Load balancer + many stateless instances; split read vs write services. — Run many small stateless copies behind a load balancer so one dying box never takes the system down, and split into a Shorten (write) and Redirect (read) service so the read fleet — 99% of traffic — scales on its own.

Add a load balancer and run many copies (horizontal scaling). Split the work into a Shorten Service (write) and a Redirect Service (read) so each scales — and fails — on its own. The read fleet, carrying 99% of traffic, can grow independently.

Scale out, not up: Instead of one giant machine, run many small stateless ones behind a load balancer. Any instance can serve any request, so one dying box never takes the system down with it.

Step 5 · A mountain of links

Where do billions of rows live?

At ~100M new links a month you cross billions of rows within a few years. No single database holds that comfortably, and one disk can’t serve the read rate on its own. Where do they live?

Design decision: You cross billions of code→url rows in a few years. Where do they live?

The call: A key-value store, sharded by a hash of the code. — The access pattern is a pure lookup by code, so a KV store is ideal; sharding by code-hash spreads billions of rows across machines, each shard small and fast. Random-looking codes spread load evenly — no hotspot.

Use a key-value store — the access pattern is a pure lookup by code — and shard it: split the keyspace across many machines, routing each code to its shard by a hash of the code. Each shard stays small and fast; add more as you grow.

Sharding: One giant table becomes many small ones spread across machines. The short code is the perfect shard key: it’s random-looking, so load spreads evenly and no single shard becomes a hotspot.

Step 6 · Count the clicks

Who clicked, from where?

Owners want stats — clicks, countries, referrers, devices. But writing an analytics row on every redirect would slow the one thing that must stay fast: the redirect itself. How do you have both?

Design decision: Owners want click stats, but writing a row on every redirect would slow it. How?

The call: Fire a lightweight click event to a stream; consumers aggregate later. — The Redirect Service emits an event to Kafka and returns immediately; downstream consumers fold events into the analytics store at their own pace. If analytics lags or crashes, redirects keep flying.

Make analytics asynchronous. The Redirect Service fires a lightweight event onto a stream (Kafka) and immediately returns the redirect. Downstream consumers fold those events into an analytics store at their own pace — the click is never blocked on counting it.

Decouple with a queue: A message stream lets a fast producer (redirects) and a slower consumer (analytics) run at different speeds. If analytics lags or crashes, redirects keep flying; events wait in the log and get processed later.

Step 7 · The sharp edges

Aliases, expiry & abuse

Real users want custom aliases (/launch), links that expire, and — less welcome — spammers who shorten malicious URLs or hammer your API.

Let writers request a custom code (check it’s free first). Store an optional TTL so expired links return 404 and get cleaned up. Add rate limiting at the gateway and a safe-browsing check on new URLs, so one bad actor can’t ruin the service for everyone.

Design for the unhappy path: Unknown code → 404. Expired link → 410. Too many requests → 429. Thinking through what happens when things go wrong is what separates a demo from a system people can trust.

You did it

You just designed a URL shortener.

  • C — l
  • B — a
  • A
  • L — o
  • A
  • A — s
  • C — u
RUN IT YOURSELF

Base-62 short codes, in Python & TypeScript

A URL shortener turns a numeric id into a short slug with base-62 encoding — and back. Here it is in both languages, running live. Switch tabs, read the comments, edit, and hit Run.

HOW TO READ THE CODE — 4 IDEAS
  1. Each new URL gets an auto-increment id (a number); the slug is that number in base 62.
  2. Encode by repeatedly taking n % 62 and dividing, prepending each digit (steps 1–2).
  3. Decode is the reverse — Horner's method walks the slug back to a number (step 3).
  4. It is a bijection: every id has one slug and vice-versa, so no collisions.
CPython · WebAssembly
built to be redirected, not memorized — make the calls, drop the cache, run the gauntlet.
Finished this one? 0 / 58 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs