System Design · step by step

Design a URL Shortener

Step 1 / 9
The numbers to beat62symbols7chars3.5Tunique codes

In the interview room

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

Functional requirements

What it must do — agree on these before drawing a single box.

  • Shorten: given a long URL, return a unique short code.
  • Redirect: GET /code sends the visitor to the original URL — the hot path.
  • Custom aliases: vanity codes like /launch, coexisting with generated ones.
  • Expiry: optional TTL — links can die on schedule.
  • Click analytics: owners see clicks, referrers, countries, devices.

Non-functional requirements

The qualities that shape the whole design — each one names the mechanism that buys it.

Redirects survive anything (availability first)
Stateless read fleet behind a load balancer; a code→url mapping is immutable, so serving slightly stale data is harmless — choose availability over consistency on the read path.
Sub-millisecond redirect latency
Cache-aside Redis holding the hot mappings in front of the store; a miss falls through and re-warms — the cache is a latency layer, never the source of truth.
Read-heavy scale (~100 reads per write)
Split the shorten (write) and redirect (read) services so the read fleet scales independently.
Billions of mappings
Key-value store sharded by a hash of the code — the access pattern is a pure key lookup, and random-looking codes spread load with no hotspots.
No two links ever collide
A counter encoded in base62, handed out by a Key Generation Service — unique by construction, no “is this taken?” check on the write path.
Analytics never slows a redirect
Fire-and-forget click events onto a stream (Kafka); analytics is best-effort and decoupled, redirects stay on the fast path.
Abuse doesn’t take you down
Rate limiting at the gateway, malicious-URL screening on writes, 404s for dead codes handled cheaply.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

302 redirectsover 301 permanent

A 301 lets browsers cache the hop — fewer hits, but you go blind: no analytics, no expiry, no kill-switch on abuse. A 302 routes every click through you; you pay servers for control. Most real shorteners pay.

Counter + base62 (KGS)over hashing the URL

Hashes collide and need retry loops; the same URL hashes identically for different owners. A dealt-out counter is unique by construction — the KGS becomes a coordination point, so it deals codes in pre-allocated batches.

Sharded key-value storeover one relational database

The query is always “code → url”, never a join. A KV store sharded by code-hash scales to billions of rows; you give up transactions you never needed.

Availabilityover strict consistency

Mappings are write-once. A redirect served from a stale cache is still correct, so the read path can favor being up over being perfectly in sync — the cheapest CAP call you’ll ever make.

What this teaches

Learn system design by building a URL shortener like Bitly or TinyURL step by step. An interactive guide covering the client–server skeleton, base62 key generation, caching the read-heavy redirect path, splitting read/write services, sharding billions of mappings, async click analytics, and the unhappy paths.

Key takeaways

  • Client → server → database — the spine shared by shorten and redirect.
  • Base62 codes from a Key Gen Service — unique by construction.
  • A Redis cache makes the read-heavy redirect path sub-millisecond.
  • Load balancer + split read/write services to scale horizontally.
  • A key-value store sharded by code holds billions of mappings.
  • Async click events via Kafka — analytics never blocks a redirect.
  • Custom aliases, TTL expiry, 404s and rate limiting for the real world.

Concepts covered

  • What is a URL shortener?
  • Two jobs, one server
  • How do we mint the code?
  • Reads dwarf writes
  • Now serve the whole internet
  • Where do billions of rows live?
  • Who clicked, from where?
  • Aliases, expiry & abuse

Design a URL Shortener — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & 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.

  • Client → server → database — the spine shared by shorten and redirect.
  • Base62 codes from a Key Gen Service — unique by construction.
  • A Redis cache makes the read-heavy redirect path sub-millisecond.
  • Load balancer + split read/write services to scale horizontally.
  • A key-value store sharded by code holds billions of mappings.
  • Async click events via Kafka — analytics never blocks a redirect.
  • Custom aliases, TTL expiry, 404s and rate limiting for the real world.
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 / 65 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