Skip to the solution
HardSystem Design16 min read

How would you design a URL shortener like TinyURL?

514 views

The problem

Question presented to candidate: "Assume the basic TinyURL design is done: we can create codes and store them. Now the product is a hit. Redirects are tens of thousands per second at peak, some links go viral, users are worldwide, and marketing wants click counts. How do you make redirects fast, cheap and reliable?"

What a strong answer should cover:

  • Why the read path dominates (about 100 redirects per create) and a capacity estimate for the redirect tier, the cache and the click stream
  • Layered caching: CDN or edge, a shared cache (cache-aside) and the replicated store; how hit rate depends on cache size under a skewed (Zipf) popularity distribution, with real numbers
  • Cache stampede on a viral or just-expired link, and the fixes: request coalescing (single-flight), early refresh, a short lock
  • 301 vs 302 as a load versus control trade-off, measured; invalidation when a link is deleted, edited or taken down; negative caching of unknown codes
  • Asynchronous click analytics through a queue so counting never slows a redirect, and multi-region read replicas for global latency

Clarifying questions expected:

  • What redirect latency and availability do we promise, and where are the users?
  • How exact must click counts be, and how fresh?
  • Can links be edited or deleted after creation, and how fast must that take effect?

Code / implementation expected: Optional โ€” a simulation of cache hit rate under skewed traffic, or of a stampede with and without request coalescing; the example does both and measures 301 vs 302.

  • url-shortener

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

16 min read ยท 16 sections

Read the code

Redirect read path: LRU hit rate under Zipf traffic, 301 vs 302 and a cache stampede with single-flight
// URL shortener read path: LRU hit rate under Zipf traffic, 301 vs 302, and a cache stampede.
const LINKS = 100000, REQUESTS = 300000, PEAK_READS = 19500;

let state = 42;
function rand() { // LCG mod 2^32, same sequence in every language
  state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
  return state / 4294967296;
}
const cdf = [];
let sum = 0;
for (let i = 1; i <= LINKS; i++) { sum += 1 / i; cdf.push(sum); } // Zipf, s = 1
function pick() {
  const x = rand() * sum;
  let lo = 0, hi = LINKS - 1;
  while (lo < hi) { const mid = (lo + hi) >> 1; if (cdf[mid] < x) lo = mid + 1; else hi = mid; }
  return lo; // link rank 0 = most popular
}
function pct(part, whole) { const t = Math.floor((part * 1000) / whole); return Math.floor(t / 10) + "." + (t % 10) + "%"; }

class LRU {
  constructor(cap) { this.cap = cap; this.map = new Map(); }
  get(k) {
    if (!this.map.has(k)) return false;
    const v = this.map.get(k); this.map.delete(k); this.map.set(k, v); return true;
  }
  put(k) {
    this.map.set(k, true);
    if (this.map.size > this.cap) this.map.delete(this.map.keys().next().value);
  }
}

console.log("== LRU cache in front of the store, Zipf traffic ==");
console.log(LINKS + " links, " + REQUESTS + " redirects");
for (const cap of [1000, 5000, 20000]) {
  state = 42;
  const cache = new LRU(cap);
  let hits = 0;
  for (let i = 0; i < REQUESTS; i++) {
    const k = pick();
    if (cache.get(k)) hits++; else cache.put(k);
  }
  const misses = REQUESTS - hits;
  const dbQps = Math.ceil((PEAK_READS * misses) / REQUESTS);
  console.log("cache " + cap + " (" + pct(cap, LINKS) + " of links): hit rate " + pct(hits, REQUESTS) + ", store reads at " + PEAK_READS + " req/s peak: " + dbQps + "/s");
}
state = 42;
const top = new Array(10).fill(0);
let topHits = 0;
for (let i = 0; i < REQUESTS; i++) { const k = pick(); if (k < 100) topHits++; if (k < 10) top[k]++; }
console.log("top 100 links get " + pct(topHits, REQUESTS) + " of traffic; #1 gets " + top[0] + " redirects, #10 gets " + top[9]);

console.log("== 301 vs 302: what the server sees ==");
for (const status of [301, 302]) {
  const browserCache = new Set();
  let serverHits = 0, staleAfterEdit = 0;
  for (let round = 0; round < 5; round++) { // the owner edits the destination before round 3
    for (let b = 0; b < 1000; b++) {
      if (status === 301 && browserCache.has(b)) { if (round >= 3) staleAfterEdit++; continue; }
      serverHits++;
      if (status === 301) browserCache.add(b);
    }
  }
  console.log(status + ": 1000 browsers x 5 clicks -> server sees " + serverHits + " clicks; clicks sent to the old destination after an edit: " + staleAfterEdit);
}

console.log("== cache stampede on a viral link ==");
const ARRIVALS = 1000, GAP_US = 40, DB_US = 20000;
for (const coalesce of [false, true]) {
  let dbReads = 0, inflightUntil = -1, cachedAt = Infinity, waited = 0;
  for (let i = 0; i < ARRIVALS; i++) {
    const t = i * GAP_US;
    if (t >= cachedAt) continue; // cache hit
    if (coalesce && inflightUntil > t) { waited++; continue; } // join the in-flight read
    dbReads++;
    if (inflightUntil < 0) { inflightUntil = t + DB_US; cachedAt = t + DB_US; }
  }
  console.log((coalesce ? "single-flight" : "no coalescing") + ": " + ARRIVALS + " requests in " + (ARRIVALS * GAP_US) / 1000 + " ms, store reads " + dbReads + ", waited on one read " + waited);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 82 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

How would you design Instagram?

Continue