SysDesignPrep.com
Interactive concepts

URL shortener codes: an interactive birthday problem for random, hashed and counter IDs

A URL shortener needs a short, unique code for every link. There are three common ways to make one: draw random characters, hash the URL and keep the first few characters, or take the next number from a counter and write it in base62. Random and hashed codes can collide, and the birthday problem says they collide far sooner than intuition suggests. Counters never collide but need coordination and produce guessable codes.

The playground draws random two-character codes into a grid of all 3,844 possibilities, then applies the same arithmetic to real code lengths and volumes.

Draw more

Two-character codes allow 3,844 values. After 73 random codes, 1 landed on a code already taken, the first after only 40: the chance of at least one collision is 49.5%, while the chance that the next code collides is 1.9%. At 7 characters there are 3.5 trillion codes. Collisions become more likely than not after only 2.2 million of them, and 1 billion links will produce about 142,000 collisions, yet any single new code collides with probability 0.0284%: generate, check, and retry the rare clash.

Every two-character code, 3,844 cells
code vIcode wRcode BHcode AAcode QKcode xmcode 4Xcode wTcode BGcode rVcode tLcode Qicode q2code Fycode Gfcode 9Ycode b4code h7code oxcode nncode A7code 7Ucode B1code zK: drawn more than oncecode pWcode Qlcode NVcode LFcode jGcode CDcode fBcode 3ucode JWcode YCcode ipcode 95code K6code lwcode yjcode Qqcode 8Scode QGcode ORcode nQcode 1zcode 4Kcode Q9code uPcode wCcode gLcode v7code Jwcode 5vcode DVcode 2Ncode rycode Dmcode Z1code qxcode sTcode lScode 2Zcode Jtcode q0code L5code iCcode yocode lzcode Qecode 9Vcode 7lcode D4
lS2ZJtq0L5iCyolzQe9V7lD4
Codes drawn7372 distinct
Collisions1first at code 40
At real scaleLengthLinks
Possible codes
3.5 trillion
Even odds of a collision after
2.2 million
Collisions to expect
142,000
Chance the next code collides
0.0284%
code takendrawn twice or more

The birthday problem, in codes

With 3,844 possible codes, drawing just 73 at random gives even odds that two match, and in the playground the first collision comes at the 40th code. The same holds at scale: 7 base62 characters give 3.5 trillion codes, but collisions become more likely than not after only 2.2 million links, and a billion links produce about 142,000 of them. That sounds alarming until you look at the other number: when a billion codes are taken, a new random code collides with probability 0.028%. So random codes work, provided every insert checks for a clash and retries, which a unique index does atomically.

Three ways to make codes

  • Random codes with a unique index and a retry are simple and unguessable. The database does the collision check, and retries stay rare as long as the space is far larger than the number of links.
  • Hashing the URL, for example the first 7 characters of a base62-encoded SHA-256, gives the same code for the same URL, which deduplicates for free. Different URLs still collide at the same birthday rate, so a clash needs a fallback such as rehashing with a salt.
  • A counter in base62 cannot collide: a billion links need only 6 characters. But the counter must be shared, through a ticket server, ranges of ids handed to each server, or Snowflake-style ids, and sequential codes let anyone enumerate every link. Scrambling the number with a reversible permutation keeps uniqueness while hiding the order.

A key generation service combines the ideas: it pre-generates unused random codes in bulk and hands batches to application servers, so creating a link needs no collision check at all.

Random codes with a retry, and base62

import secrets, string

ALPHABET = string.digits + string.ascii_lowercase + string.ascii_uppercase   # base62

def random_code(length=7):
    return "".join(secrets.choice(ALPHABET) for _ in range(length))

def shorten(db, url, tries=5):
    for _ in range(tries):
        code = random_code()
        # the unique index on code makes the collision check atomic
        inserted = db.execute(
            "INSERT INTO links (code, url) VALUES (%s, %s) ON CONFLICT (code) DO NOTHING",
            (code, url)).rowcount
        if inserted:
            return code
    raise RuntimeError("no free code after retries")

def base62(n):
    """For counter-based ids: unique by construction, no check needed."""
    s = ""
    while True:
        n, r = divmod(n, 62)
        s = ALPHABET[r] + s
        if n == 0:
            return s

In a system design interview

Do the arithmetic out loud: “100 million new links a month is about 6 billion over five years. Seven base62 characters give 3.5 trillion codes, so a random code collides with an existing one less than 0.2% of the time even at the end. We generate randomly, insert under a unique index, and retry on conflict. Codes are unguessable, which matters because some links are private.” Expect the follow-ups: custom aliases, the same long URL shortened twice, and how the check works when the table is sharded by code.

Read the guide to hashing and encoding or unique ids, or watch a strong candidate design a URL shortener.

Questions people ask

How long should URL shortener codes be?

Long enough that the code space dwarfs the number of links. Seven base62 characters give about 3.5 trillion codes, so even billions of links leave a new random code a small chance of colliding. Counter-based codes can be shorter: a billion links fit in six characters.

What is the birthday problem?

The surprising result that among random draws from a large space, a repeat appears after only about the square root of the space size. With 365 birthdays, 23 people give even odds of a shared birthday; with 3.5 trillion short codes, about 2.2 million random codes give even odds of a collision.

How do you handle short code collisions?

Insert the new code under a unique index and, if the insert conflicts, generate another and retry. Because a new code collides with probability equal to the share of codes already taken, retries stay rare even when collisions somewhere are certain.

Should a URL shortener hash the URL or use random codes?

Hashing gives the same code for the same URL, which deduplicates repeated links, but truncated hashes of different URLs collide at the same rate as random codes, so a fallback is still needed. Random codes are simpler and unguessable; many designs use random codes and a separate lookup for duplicate URLs if deduplication matters.

What about using a counter and base62?

Encoding an auto-incrementing id in base62 guarantees unique, short codes with no collision check. The costs are coordinating the counter across servers, through a ticket server, id ranges or Snowflake-style ids, and codes that are sequential, so anyone can enumerate links unless the ids are scrambled.

What is a key generation service?

A service that pre-generates unused random codes and hands them to application servers in batches. Creating a link then just takes the next code from the local batch, with no collision check, at the cost of running and coordinating the service and losing a few unused codes if a server dies.

What is base62 encoding?

Writing a number in base 62 using the digits 0 to 9 and the letters a to z and A to Z, which are all safe in URLs. Each character holds about 5.95 bits, so 7 characters cover numbers up to about 3.5 trillion.