Let’s talk
World 1 · URL Shortener
All steps
  1. 1What we are building
  2. 2Hash or counter
  3. 3The ID collision
  4. 4The stale redirect · soon
  5. 5Analytics on the hot path · soon
  6. 6The vanity alias race
  7. 7Cleanup at scale · soon
  8. 8Build it · soon

Hash or counter

Every long URL needs a short code. Where does that code come from?

Before this: What we are building·Next: The ID collision

In 30 seconds

The problem
Every long URL needs a short, unique code, like g8 in sho.rt/g8.
Option A: hash
Hash the URL and keep 7 characters. Simple, but two URLs can get the same code, so you must check and retry.
Option B: counter
Give each link the next number and write it in base62. It never collides, but every new link asks one central counter.
Common choice
The counter, then make it scale by giving each server its own block of numbers.
In an interview
Hashing avoids a central counter but needs collision handling. A counter avoids collisions but must be scaled. Start with a counter, then hand out ID ranges.

Why it matters

Every short link depends on its code. If two links share one, visitors land on the wrong page, and nobody notices until someone complains.

This choice also decides what you will have to scale later: a collision check on every create, or one counter that every create waits for.

Words you need

Short code
The few characters after the domain in a short link, like g8 in sho.rt/g8.
Hash
A function that turns any input into a fixed-length jumble of characters. The same input always gives the same output.
Collision
Two different inputs ending up with the same code.
Birthday paradox
Shared codes show up far sooner than you would guess. With N possible codes, a collision is likely after only about √N items.
Base62
Writing a number with 62 symbols (0–9, a–z, A–Z), so big numbers become short codes. Seven characters cover about 3.5 trillion numbers.
Sequence (counter)
A database counter that hands out the next number, exactly once, every time it is asked.
Bottleneck
The one part that everything else has to wait for.

Picture it

Follow one new link through each option.

Option A: hash

  1. Alice pastes a long URL
  2. md5 gives 32 hex characters
  3. keep the first 7
  4. that is her code

Seven hex characters allow only about 268 million codes. By the birthday paradox, there is already a 50% chance that two links share a code after about 19,000 links. We expect 100 million a month.

Option B: counter

  1. Alice's link is number 1,000
  2. base62 → g8
  3. Bob's link is 1,001
  4. base62 → g9

Two numbers are never equal, so two codes never are. The catch: every new link has to ask the same counter for its number.

The options

Option A: hash, keep 7 characters

code = md5(longUrl).slice(0, 7)

Good: Simple and stateless. The same URL always gives the same code.

Cost: Different URLs can share a code. Every create must check and retry, and that gets more likely as links pile up.

Option B: counter + base62

code = toBase62(nextId())

Good: Unique by construction. Each number is handed out once.

Cost: Every create asks one central counter. Under heavy writes it becomes the bottleneck, and the next step breaks exactly this.

The choice and its cost

The common interview answer is the counter: correctness first, and collisions become impossible instead of rare.

Then fix its weak spot. Instead of asking the database for every number, each server reserves a block, say 1,000 IDs at a time, and hands them out from memory. The database is asked once per block, not once per link.

What it costs

Codes come out in order (g8, g9, ga…), so anyone can guess the next one. If links should not be guessable, shuffle the number before encoding it. That is a later step.

Interviewers also listen for: it's read-heavy

Redirects far outnumber new links, often by about 100 to 1. Design for reads first (a cache, a CDN in front), then worry about the write path.

Check yourself

  1. 1Why can two different URLs get the same code with option A?Show answer

    We keep only 7 characters of the hash, so there are far fewer codes than possible URLs. With md5's hex output that is about 268 million codes, and by the birthday paradox a shared code is likely after only about 19,000 links.

  2. 2Why can the counter never collide?Show answer

    Each number is handed out exactly once, and base62 is just another way of writing a number. Different numbers always give different codes.

  3. 3What is the counter's weak spot, and one way to fix it?Show answer

    Every new link waits on one central counter. Give each server a block of IDs to hand out from memory, so it asks the database only once per block.

The code

GitHub

hash-codes.ts

import { createHash } from "node:crypto";
import type { Pool } from "pg";

// Option A: hash the URL and keep the first 7 characters.
export function hashCode(input: string): string {
  return createHash("md5").update(input).digest("hex").slice(0, 7);
}

// Two URLs can share a code, so every create checks and retries.
export async function createLink(db: Pool, longUrl: string): Promise<string> {
  for (let attempt = 0; attempt < 5; attempt++) {
    const code = hashCode(attempt === 0 ? longUrl : `${longUrl}#${attempt}`);
    const saved = await db.query(
      `INSERT INTO links (code, target) VALUES ($1, $2)
       ON CONFLICT (code) DO NOTHING`,
      [code, longUrl],
    );
    if (saved.rowCount === 1) return code; // the code was free
    // The same URL shortened twice also ends up here.
  }
  throw new Error("No free code after 5 tries");
}

Example code. The tested version ships with the reference URL shortener in the repo.

Every trap in this world is runnable

The design notes are in the repo, and each trap that follows ships with the bug, the fix and a test that proves it.

Watch the 60-second version

Remember

A hash can collide and a counter can't, but a counter has to be scaled. Say both halves out loud.

NextThe ID collisionTwo requests read the counter at the same instant and get the same code.