Low-Level Design · lesson 9 of 18
Beginner
4Intermediate
10- 5 Design a Parking Lot: Allocating and Freeing Spots
- 6 Design an Elevator System: Scheduling Requests
- 7 Design a Library Management System: Borrowing and Returning Books
- 8 Design an Expense-Splitting App: Balances and Minimal Settlements
- 9 Design a URL Shortener: Sequential Codes vs Real Hash Collisions
- 10 Wrapping Objects: Adapter, Facade, and Proxy in Python
- 11 The Composite Pattern in Python: Sizing a File Tree Without isinstance
- 12 The State Pattern in Python: Refactoring a Vending Machine Into State Classes
- 13 Undo and Redo in Python: the Memento and Command Patterns Compared
- 14 Chain of Responsibility in Python: Expense Approvals Without an if/elif Ladder
Advanced
4- 15 Concurrency in LLD: A Real Race Condition in the Parking Lot, Then the Fix
- 16 The Observer Pattern in Practice: A Stock Ticker Notifying Multiple Displays
- 17 Design Patterns That Are Just Language Features: Strategy, Command, Iterator
- 18 Abstract Factory and Prototype in Python: Matching Families and Cloning Objects
Also part of Low-Level Design
12- + SOLID Principles in Java from Java
- + Composition vs Inheritance in Java from Java
- + Design Patterns in Java from Java
- + Python Design Patterns from Python
- + Design Patterns in TypeScript from TypeScript
- + JavaScript Design Patterns from JavaScript
- + Design Patterns in C# from C#
- + Design Patterns in C++ from C++
- + Design Patterns in Go from Go
- + Design Patterns in Rust from Rust
- + Abstract Classes in Java from Java
- + Sealed Classes and Interfaces in Java from Java
Also part of: System Design
Design a URL Shortener: Sequential Codes vs Real Hash Collisions
Two ways to shorten a URL — a base62 counter that never collides but leaks a pattern, and a hash that needs real collision handling.
By SkillByExample · Published
A URL shortener’s whole job is compressing a long string into a short one you can always map back — the interesting design decision is how you generate that short code.
Sequential ID, base62-encoded: never collides, always guessable
import string
ALPHABET = string.digits + string.ascii_lowercase + string.ascii_uppercase # base62, 62 chars
def encode(n):
if n == 0:
return ALPHABET[0]
chars = []
base = len(ALPHABET)
while n > 0:
n, rem = divmod(n, base)
chars.append(ALPHABET[rem])
return "".join(reversed(chars))
class SequentialShortener:
def __init__(self, start_id=1):
self.next_id = start_id
self.urls = {}
def shorten(self, long_url):
code = encode(self.next_id)
self.urls[code] = long_url
self.next_id += 1
return code
shortener = SequentialShortener(start_id=60)
print(shortener.shorten("https://example.com/a"))
print(shortener.shorten("https://example.com/b"))
print(shortener.shorten("https://example.com/c"))
print(shortener.shorten("https://example.com/d"))
$ python sequential_shortener.py
Y
Z
10
11
Starting the counter at 60 (instead of 1) makes the carry visible immediately: 60 and 61 are
still single base62 digits (Y, Z — the alphabet is digits, then lowercase, then uppercase,
62 symbols total), but 62 has to roll over into two digits, the same way 9 rolls over into 10
in decimal. Anyone who shortens two URLs a minute apart and gets back Y and Z has just
learned the codes are sequential — incrementing either one finds the next real shortened URL.
Hash-based codes: unguessable, but collisions are mathematically guaranteed
import hashlib
def short_hash(url, salt=""):
return hashlib.md5((url + salt).encode()).hexdigest()[:2] # 2 hex chars = 256 possible codes
class HashShortener:
def __init__(self):
self.codes = {} # code -> long_url
def shorten(self, long_url):
salt = ""
attempt = 0
while True:
code = short_hash(long_url, salt)
if code not in self.codes:
self.codes[code] = long_url
if attempt > 0:
print(f" {long_url} -> {code} (needed {attempt} retry(s))")
else:
print(f" {long_url} -> {code}")
return code
if self.codes[code] == long_url:
return code
attempt += 1
salt = str(attempt)
print(f" collision on {code!r} ({self.codes[code]!r} already has it) - retrying with salt {salt!r}")
shortener = HashShortener()
shortener.shorten("https://example.com/page-4")
shortener.shorten("https://example.com/page-11")
$ python hash_shortener.py
https://example.com/page-4 -> 5c
collision on '5c' ('https://example.com/page-4' already has it) - retrying with salt '1'
https://example.com/page-11 -> 8c (needed 1 retry(s))
page-4 and page-11 are genuinely different URLs, but truncating MD5 to just 2 hex
characters leaves only 256 possible codes — page-11 really does hash to the same 5c that
page-4 already claimed. The fix isn’t to avoid collisions (with a small enough code space,
you can’t); it’s to detect the collision and retry with a salted input until a free code
turns up. A production system uses a longer code (6-7 base62 characters gives billions of
values), which makes collisions rare rather than absent — the retry logic here is the same
logic that code would still need, just triggered far less often.
Practice
1. Confirm the sequential shortener really is guessable: given only the code Z, predict the next code without calling shorten again.
import string
ALPHABET = string.digits + string.ascii_lowercase + string.ascii_uppercase
def encode(n):
if n == 0:
return ALPHABET[0]
chars = []
base = len(ALPHABET)
while n > 0:
n, rem = divmod(n, base)
chars.append(ALPHABET[rem])
return "".join(reversed(chars))
# Z is index 61 in ALPHABET (0-9, then a-z, then A-Z)
z_index = ALPHABET.index("Z")
print(f"Z's numeric ID: {z_index}")
print(f"Next code: {encode(z_index + 1)}")
$ python predict_next.py
Z's numeric ID: 61
Next code: 10
ALPHABET.index("Z") recovers the exact integer that produced Z, and the shortener’s
next_id is always exactly one more than whatever the previous call returned — so anyone
holding one valid code can compute the next one without ever touching the service’s database.
2. Widen the truncated hash to 3 hex characters (4096 possible codes) and confirm the same two URLs from the lesson no longer collide.
import hashlib
def short_hash(url, length=3):
return hashlib.md5(url.encode()).hexdigest()[:length]
for url in ["https://example.com/page-4", "https://example.com/page-11"]:
print(f"{url} -> {short_hash(url)}")
$ python wider_hash.py
https://example.com/page-4 -> 5c6
https://example.com/page-11 -> 5c3
With 3 hex characters instead of 2, page-4 and page-11 land on 5c6 and 5c3 — they
still share the same first two characters (5c, which is exactly why they collided at
length=2), but as full 3-character codes they’re different, so no collision this time. The
collision in the main example wasn’t a special property of those two URLs; it was a property
of how small (256 values) the truncated hash space was.
3. Find a different pair of URLs (from a range of "https://example.com/item-N") that collide in the 2-hex-character space, without hardcoding the answer.
import hashlib
def short_hash(url):
return hashlib.md5(url.encode()).hexdigest()[:2]
seen = {}
for i in range(1, 500):
url = f"https://example.com/item-{i}"
code = short_hash(url)
if code in seen:
print(f"Collision: {seen[code]!r} and {url!r} both hash to {code!r}")
break
seen[code] = url
$ python find_another_collision.py
Collision: 'https://example.com/item-6' and 'https://example.com/item-15' both hash to '92'
A different URL pattern (item-N instead of page-N) collides on a completely different
pair, at a different point — confirming the earlier collision wasn’t cherry-picked to be the
only one; a 256-value code space collides quickly and repeatedly no matter which real strings
feed it.
4. Shorten the same URL twice through HashShortener and confirm it returns the same code both times instead of treating it as a fresh collision.
import hashlib
def short_hash(url, salt=""):
return hashlib.md5((url + salt).encode()).hexdigest()[:2]
class HashShortener:
def __init__(self):
self.codes = {}
def shorten(self, long_url):
salt = ""
attempt = 0
while True:
code = short_hash(long_url, salt)
if code not in self.codes:
self.codes[code] = long_url
return code
if self.codes[code] == long_url:
return code
attempt += 1
salt = str(attempt)
shortener = HashShortener()
first = shortener.shorten("https://example.com/page-4")
second = shortener.shorten("https://example.com/page-4")
print(f"first call: {first}, second call: {second}, same code: {first == second}")
$ python idempotent_shorten.py
first call: 5c, second call: 5c, same code: True
The if self.codes[code] == long_url: return code check is what makes shortening the same
URL twice idempotent — without it, the second call would see its own code already taken by
itself, treat that as a real collision, and hand back a different code for the identical URL.