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 numbers

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

writes, average (100M ÷ 86,400)
1,200 /s
reads, average — 350 K at peak
120 K /s
storage over 5 years at ~500 B a link
90 TB
codes available at 7 base-62 chars
3.5 T

Worked the way back-of-envelope estimation describes: a few facts, a few multiplications, and the big number tells you the shape of the problem.

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

How a redirect flows

BrowserRedirect serviceCacheStoreGET /x7Kq2PaGET code:x7Kq2Pahttps://long…hit — ~95% of the time301 / 302 Location: https://long…publish click event (async)analytics never delays the redirecton a miss only: SELECT url WHERE code = …
The hot path is one cache lookup and a redirect. Everything else is kept off it.

Decision 1: how to make the short code

Hash the URL
MD5, take 7 chars

Simple and stateless. But two different URLs will produce the same 7 characters eventually (a collision), so every write needs a read-before-write to check — and the collision path is fiddly.

Counter + base 62

1, 2, 3… encoded as a–z A–Z 0–9. Guaranteed unique, no check needed. But one counter is one thing every server must talk to, and sequential codes are guessable — anyone can walk your whole link database by counting up.

Lease ranges from a counter
the diagram

Each service instance grabs a block — say [1,000,000, 1,001,000) — from a small coordination service, then hands out codes locally with no network call. Unique by construction, unlimited throughput, codes no longer densely sequential.

Base-62 encoding and range leasingTypeScript
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

function toBase62(n: number): string {
  let s = "";
  do { s = ALPHABET[n % 62] + s; n = Math.floor(n / 62); } while (n > 0);
  return s.padStart(7, "0");
}

class CodeSource {
  private next = 0; private end = 0;
  async take(): Promise<string> {
    if (this.next >= this.end) {
      // One coordination call per 1,000 codes — a crash wastes the rest of a block. Who cares.
      const start = await coordinator.reserveBlock();
      this.next = start; this.end = start + ;
    }
    return toBase62(this.next++);
  }
}

Decision 2: keep the read path empty

The redirect service does one lookup and returns a redirect. That is the entire hot path. Everything tempting to add — click analytics, spam checks, per-user counters — is deliberately kept off it. Analytics goes out 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 few links get most of the clicks. That is exactly the shape a cache loves. And because a code's mapping never changes once written, the usual hard part of caching — knowing when a copy has gone stale — does not exist here. Cache forever.

The whole redirect handlerTypeScript
app.get("/:code", async (req, res) => {
  const url = (await cache.get(req.params.code)) ?? (await store.lookupAndCache(req.params.code));
  if (!url) return res.status(404).end();
  clicks.publish({ code: req.params.code, at: Date.now(), ua: req.headers["user-agent"] }); // no await
  res.redirect(302, url);
});

Decision 3: 301 or 302

301 Permanent

The browser caches the mapping, so repeat visits never reach you at all — a large, free traffic reduction. But you lose click analytics for those visits, and you can never re-point or revoke the link for people who already have it.

302 Temporary

Every visit comes to you. Full analytics, links can be changed or disabled. You pay the traffic. Most commercial shorteners pick this — analytics is the product.

Where this design breaks

  • Cache cold start. Restart the cache fleet and 120 K reads/s hit the store directly. Stagger restarts, or warm from a list of recent hot keys.
  • One link going viral. A single key can exceed what one cache node can serve. Replicate that key across nodes, or put a CDN in front (the redirect is cacheable).
  • Custom aliases. short.ly/my-campaign breaks the collision-free property of leased ranges — now you need a real uniqueness check, on a separate path from generated codes.
  • Expiry and reuse. Nothing here reclaims codes. If links expire, sweep them by TTL — and never reissue an expired code: old copies in the wild would silently point somewhere new.

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, 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.