Problem library

Track 1 · Foundations

easy

URL Shortener

Turn a long URL into a short code and redirect on lookup. The classic warm-up: tiny write path, enormous read path, and a key-generation problem hiding underneath.

7 parts · 2 workloads
read-heavykey generationcachingKV store

Suggested architecture

Scenario

Two paths that have nothing in common. Reads outnumber writes 100:1, so the question is how much of the read path the cache absorbs — and what the store sees when it does not.

Links followed per second. This is the hot path; everything else is sized from it.

Links created per second. Small, but every one is a durable write.

Share of redirects the cache answers. Popularity is skewed, so a small cache gets a high number — until it restarts.

Stateless, so this is the knob you turn first.

Each partition of the key-value store handles a fixed rate before you shard again.

Open in playground →This diagram is a playground design: the sliders write onto it and the same engine judges it. Open it to change anything, run a spike, or price it.
Entering
~121K req/s
Redirects
17 ms
New links
17 ms
Busiest
API Gateway 61%
Storage, 5 years
~86 TB
synchronousasynchronousfallback / miss path
requestsPOSTGETnext codeinsertlookupcache miss
Components

Click a component for its role, common technology choices and tradeoffs, and what it is carrying at this scale. Hover a connection to see what flows along it. Drag to rearrange — layout changes are local and reset on reload.

Every figure here is a rough estimate from simple capacity arithmetic, not a benchmark. Each part carries its own assumption about what one copy can do — real numbers depend on your hardware, payloads and access pattern. The point is which component moves first as you turn the dials, not the digits themselves.

In plain words

A URL shortener turns a long address into a short one and sends anyone who opens the short one to the long one. Making a link happens once; opening it happens millions of times. Nearly everything in this design comes from treating those two actions differently — and the lesson (find the busy path, then make it as small as possible) is one you will reuse in almost every system.

The shape of the problem

A URL shortener looks trivial and mostly is — but it is the cleanest example of a system whose two operations have nothing in common. Creating a link is a rare, write-once operation where nobody minds an extra half second. Following a link is constant, read-only, and every millisecond is felt. Almost every design decision here follows from refusing to treat those two paths the same way.

Assume the interview numbers: 100 M new links per day and a 100:1 read/write ratio.

  • Writes: 100 M / 86 400 ≈ 1 200 writes/sec average, call it 3 500 at peak.
  • Reads: ≈ 120 K reads/sec average, 350 K at peak.
  • Storage: 100 M/day × ~500 bytes × 5 years ≈ 90 TB.

The write number is small enough that a single well-tuned database handles it. The read number is not, and that is where the design goes.

Generating the short code

Three approaches, and the choice matters more than it looks.

Hash the URL (MD5/SHA, take the first 7 chars). Simple and stateless, but collisions — two different URLs producing the same code — are certain at scale, so every write needs a read-before-write to check — which turns a cheap insert into a round trip, and the collision-resolution path is fiddly.

Auto-increment a counter, encode base62. Guaranteed unique, no collision check needed. But a single counter is a coordination point (one thing every server must talk to, which caps speed and fails alone), and sequential codes are guessable — anyone can walk your entire link database by counting up.

Lease ranges from a counter. Each service instance grabs a block (say [1 000 000, 1 001 000)) from a coordination service — a small consensus system — then hands out codes from that block locally with no network calls. Uniqueness comes for free from the disjoint ranges, throughput is unbounded, and codes are no longer densely sequential. This is what the diagram shows.

Base62 (a–z, A–Z, 0–9) at 7 characters is 62⁷ ≈ 3.5 trillion codes. You can burn ranges carelessly on every crash and still not run out.

Why the read path is almost empty

The redirect service does one lookup and returns a 301. That is the entire hot path, and everything tempting to add — click analytics, spam checks, per-user counters — is deliberately kept off it. Analytics goes out as fire-and-forget (sent without waiting for a reply) or onto a queue; the user's redirect never waits for it.

Link popularity is very uneven: a small number of links take a large share of the traffic (a power law, if you want the name). That is exactly the shape a cache is good at. And because a short code's mapping is immutable once written, the usual hard part of caching — invalidation, knowing when a cached copy has gone stale — simply does not exist here.

301 vs 302

A permanent redirect (301) lets the browser cache the mapping, so repeat visits never reach you at all. That is a large, free traffic reduction. The cost is that you lose per-click analytics for cached visits, and you can never re-point or revoke that link for users who already have it. A temporary redirect (302) gives you both of those back and gives up the traffic reduction. Most commercial shorteners choose 302 for exactly this reason — analytics is the product.

Where this design breaks

  • Cache cold start. Restart the cache fleet and 120 K reads/sec hit the store directly. Stagger restarts, or warm from a recent-keys list.
  • A single link going viral. One key can exceed what one cache shard (one of the machines the cache is split across) can serve. The fix is replication of that key across shards, or an edge cache / CDN in front.
  • Custom aliases. They break the collision-free property of range leasing — now you genuinely need a uniqueness check, on a separate path from generated codes.
  • Deletes and expiry. Nothing in this design reclaims codes. If links expire, you need a TTL sweep and a decision about whether expired codes may be reissued (they should not be — old links in the wild would silently re-point).

Take this with you

  • The one idea: separate the rare write path from the constant read path, and keep the read path down to a single lookup.
  • In an interview, lead with the numbers (1 200 writes/s vs 120 K reads/s), pick range-leasing for codes, and be ready to defend 301 vs 302.
  • At work, look for the same shape in your own system: a hot path with extras bolted on. Every one of them is latency you are paying on every request.