Skip to main content
URL Shortenerlesson 2 of 4 · 3 min read

Short Codes and the Data Model

Two ways to mint a code

Decide how you mint the code, the core decision here, and there are two defensible answers.

Encode a unique number, the first of them. Take an identifier from a sequence and encode it in an alphabet of letters and digits. You get a code needing no collision checking at all, because the numbers underneath never repeat.

Do the capacity arithmetic: seven characters from 62 options is about 3.5 trillion codes, which covers 3,500 years at a hundred million links a month.

Watch the drawback: leakage. Sequential numbers mean one code followed by the next tells your competitors how many links you create a day, and it makes codes guessable. Randomising within a pre-allocated range fixes both cheaply.

Hash the address instead, the second answer. Take a hash of the long address and keep the first seven characters. Identical addresses now deduplicate to one code for free, which numbering cannot do.

Pay for that in collisions, because seven characters of a hash will eventually match two different addresses. Every insert has to check and retry with something appended on conflict, and at 40 writes a second that check is cheap.

The data model

Keep the data model deliberately dull. One table with the code as the key, the long address, a creation time, an expiry and an owner. No joins anywhere on the hot path.

Notice the shape you ended up with: a pure key-value lookup. That is why the wide-column stores fit at very large scale. It is also why an ordinary relational database with the code as its key serves tens of thousands of reads a second without complaining.

Build two endpoints. One takes a long address and an optional custom alias, returns the short one, and rate limits per user so nobody scripts a billion links. The other does the redirect. Custom aliases live in the same table behind a uniqueness constraint, first come first served.

the shape of it
ClientLink APIPOST /api/linksID sequencebase62 encodeLinks tablecode -> long URL1. long URL2. next ID3. insert mapping4. short URL
step 1 of 4
The write path mints a unique ID, encodes it in base62, and stores one row; no collision check needed.
turning a number into seven characters
Java
static final String ALPHABET =
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

String encode(long id) {
  StringBuilder sb = new StringBuilder();
  while (id > 0) {
    sb.append(ALPHABET.charAt((int) (id % 62)));
    id /= 62;
  }
  return sb.reverse().toString();
}
// 62^7 is about 3.5 trillion, so seven characters covers 3,500 years
// at a hundred million links a month.

// Sequential ids leak your volume and make codes guessable, so take
// the id from a shuffled range rather than a bare counter.

Worked example

Marcus builds the encoder for an internal shortener at a fintech and picks MD5-prefix codes because retailers submit the same promotional URL hundreds of times and dedup saves rows. Six months in, at 80 million stored links, the on-call channel lights up: a merchant's link resolves to a different merchant's page. It is the first real collision, two URLs sharing the prefix 'x9Kd21Q'. The insert path had a check-then-write race: two app servers checked for the code in the same 3 ms window, both saw nothing, both inserted, and the second overwrote the first. His fix is an INSERT with ON CONFLICT that compares the stored long_url; on mismatch it appends a counter to the input and rehashes. He backfills a scan for other silent overwrites and finds 4 more. Collision handling was in his design doc; the atomicity of it was not.