Design a URL Shortener (TinyURL)

The canonical warm-up system design: base-62 over a unique ID, a 100:1 read-heavy cache path, the 301-vs-302 redirect trade-off, and how to mint codes with zero collisions across many servers — interviewed everywhere from SDE-1 screens to Staff loops.

system-designcase-studyhashingcachescalability

Prompt

Design a service that turns a long URL into a short one (e.g. tiny.cc/3xZ9a), redirects visitors to the original, and supports custom aliases, expiry, and click analytics. Reads dwarf writes (~100:1); the system holds billions of links and redirects must feel instant.

Why this one, and at what level

It's the most common warm-up because it's small enough to finish yet has one real decision (how to mint codes) and one real constraint (read-heavy at scale). SDE-1: a correct API, schema, and the base-62 idea. SDE-2: capacity math, the cache path, 301-vs-302, and sharding. SDE-3 / Staff: collision-free distributed ID generation, the hot-key problem, an analytics pipeline, and abuse/expiry — with trade-offs named. Asked at Amazon, Google, Microsoft, Uber, Stripe, Atlassian and most startups.

1. Requirements

Functional: create a short code for a long URL; redirect a short code to its long URL; optional custom alias; optional expiry; click analytics. Non-functional: redirects under ~10 ms p99 (read-optimized); very high availability (a dead redirect breaks every link ever shared); codes not trivially guessable; durable — links must not be lost.

2. Capacity estimate (say the numbers out loud)

  • Writes: ~100M new links/month ≈ 40 writes/s (peak ~10×).
  • Reads: 100:1 ⇒ ~4,000 reads/s (peak ~40k/s) — this is the system to optimize.
  • 5-year horizon: ~6B links. Code length in base-62 ([0-9a-zA-Z]): 62⁶ ≈ 57B, 62⁷ ≈ 3.5T — so 7 characters is comfortable headroom.
  • Storage: 6B × ~500 bytes (URL + metadata) ≈ 3 TB — fits a sharded store; the hot set fits in memory. (See system-design-numbers.)

3. API & data model

POST /urls        { longUrl, customAlias?, ttlDays? }  -> { shortCode }
GET  /{shortCode}                                       -> 301/302 redirect
GET  /urls/{shortCode}/stats                            -> { clicks, ... }
links( short_code PK, long_url, created_at, expires_at, owner_id )

A key-value store fits the access pattern perfectly: the only hot query is short_code -> long_url. Postgres works early; the natural scale-out is a sharded KV (DynamoDB / Cassandra) keyed on short_code.

4. The core decision — minting the short code

Everything interesting is here. Three approaches:

ApproachIdeaProblem
Hash + truncatebase62(md5(longUrl))[:7]Collisions → must check-and-retry on every write; same URL maps to same code (sometimes unwanted)
Random 7 charspick from 62⁷Collisions rare but possible → still a read-before-write check
Counter + base-62take a unique 64-bit ID, base-62 encode itZero collisions by construction; only catch is sequential IDs are guessable

The winner is encode a unique ID. Base-62 is just a base change — the same trick as writing a number in hex, with 62 digits:

const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
function base62(id: number): string {
  let s = "";
  while (id > 0) { s = ALPHABET[id % 62] + s; id = Math.floor(id / 62); }
  return s || "0";
}

The real interview pivot: where does a globally-unique ID come from without a single counter becoming the bottleneck or a single point of failure? Hand each app server a pre-allocated range of IDs (e.g. a ticket server leases blocks of 1,000), or use a Snowflake-style ID (timestamp + machine id + sequence). That sub-problem is big enough to get its own page — see distributed-unique-id. To kill guessability, encode a Snowflake ID or XOR the counter with a fixed secret so codes don't run …3x9, …3xA, …3xB.

5. The read path — where all the traffic is

Reads are 100× writes, so the redirect path must be cache-first:

  • Cache-aside (Redis): on redirect, look up the code in Redis; on a miss, read the DB and populate the cache. The working set of "links people actually click" is tiny versus the 6B total, so hit rates are high. (See caching.)
  • 301 vs 302 — the classic trade-off. A 301 (permanent) is cached by browsers and proxies, so repeat visits never reach you — fastest, but you lose click analytics. A 302 (temporary) sends every visit back to your server, so you can count clicks and change the destination, at the cost of more traffic. If analytics matter, use 302.
Request flow — LB → stateless app → cache → DBtime cache hit ≈ O(1)space
requestGET /u/7
Client
LB
app ×3
012
Cache
DB

1/11GET /u/7: the load balancer routes to app server 0 (round-robin). Servers are stateless, so any one can serve it.

cached keys = 0→ server = 0

6. Scaling the store

  • Shard by short_code (hash partitioning). Lookups are single-key, so they route to exactly one shard — no scatter-gather.
  • Consistent hashing so adding a shard moves only ~1/N of the keys instead of reshuffling everything. Step through why below. (See databases-at-scale.)
Consistent hashing — add a node, move ~1/N keystime O(log N) lookupspace O(N + keys)
user1cart9ord42img7sess3u88N1N2N3

1/83 nodes on the ring. Each key belongs to the next node clockwise from its hash position — that's the whole rule.

nodes = 3
  • Replication for availability — read replicas absorb the read fan-out; the cache shields the primary.

7. Deep dives (the follow-ups)

  • Custom aliases: write to the same table with a uniqueness constraint; reject on conflict. Keep them in a separate namespace so they can't collide with generated codes.
  • Analytics at scale: don't write to the DB on every click. Emit a click event to a queue/stream (Kafka), aggregate asynchronously, and store counts separately — the same pattern as ad-click-aggregator. This keeps the redirect path at one cache read.
  • Expiry: store expires_at; treat expired codes as 404 (lazy) and sweep them with a background job. TTL the cache entry to match.
  • Abuse & safety: rate-limit creation per user/IP (see rate-limiting); scan submitted URLs against a malware/phishing blocklist before issuing a code.
  • The hot key: one viral link can hammer a single cache shard. Mitigate with local in-app caching of ultra-hot codes and/or replicating that key across cache nodes.

Design drills

Design drills: URL Shortener0/4 done

Whiteboard each one out loud for 5–10 minutes before you reveal what a strong answer covers — the gap between your sketch and the checklist is your study list. Progress is saved on this device.

Core

Generate globally-unique short codes across 50 app servers with no central counter as a bottleneck.

Warm-up

Explain 301 vs 302 and pick one for a product that bills on click counts.

Stretch

A single link goes viral — 50k req/s to one short code. Keep redirects fast.

Stretch

Design the click-analytics pipeline without slowing the redirect.