How to Choose a URL Shortener Strategy in System Design
The whole url shortener system design comes down to one decision: how you generate the short code. Random 6-char codes hit their first collision around 240,000 URLs, and hashing the URL forces two users to share one code. The strategy that survives to 100M URLs is base62 of an auto-increment row id - unique by construction, ~7 chars, no retry loops.
The one decision that defines the design
"Design TinyURL" almost always reduces to a single question: how do you turn a long URL into a short code? The target workload is 100M URLs stored, ~1000 reads/sec, ~10 writes/sec, reads hugely outnumbering writes. Three short-code strategies all work in a demo, but only one holds up at that scale.
Each strategy is a full FastAPI service backed by the same table:
CREATE TABLE urls (
id BIGSERIAL PRIMARY KEY,
short_code TEXT UNIQUE,
long_url TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
They differ only in the short-code generator.
Strategy A - random 6 characters (collides at scale)
Pick 6 random base62 characters and retry on a unique-constraint violation:
BASE62 = string.ascii_letters + string.digits # 62 chars
def random_code(length=6):
return "".join(random.choices(BASE62, k=length))
6 characters gives ~56 billion combinations, which sounds plenty. But by the birthday paradox you expect your first collision around 240,000 URLs - well before 1M of the 100M target. Past that point every insert runs a retry loop, and the loop only tries 10 times before returning a 500. Random codes are fine for a toy, wrong for the workload.
Strategy B - SHA-256 hash of the URL (kills per-shorten analytics)
Hash the URL and take the first 8 hex characters, inserting with ON CONFLICT DO NOTHING:
def hash_code(url: str) -> str:
return hashlib.sha256(url.encode()).hexdigest()[:8]
The same URL always maps to the same code. That looks like free deduplication, but it breaks the product: two different users shortening the same link share one code. You cannot track clicks per shorten, one user cannot delete their link without deleting everyone else's, and you cannot later point two users at different destinations. It also still collides at scale, and hex only uses 16 of 62 available characters, wasting code space.
Strategy C - base62 of the auto-increment id (the answer)
Let the database mint a unique id on insert, then encode that id in base62:
BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def to_base62(n: int) -> str:
if n == 0:
return BASE62[0]
result = []
while n:
result.append(BASE62[n % 62])
n //= 62
return "".join(reversed(result))
@app.post("/api/shorten")
def shorten(req: ShortenRequest):
with DB.cursor() as cur:
cur.execute("INSERT INTO urls (long_url) VALUES (%s) RETURNING id", (req.url,))
row_id = cur.fetchone()[0]
code = to_base62(row_id)
with DB.cursor() as cur:
cur.execute("UPDATE urls SET short_code = %s WHERE id = %s", (code, row_id))
return {"short_code": code, "url": f"/{code}"}
Every row gets a unique primary key, so the code is unique by construction - no retry loops, no collision checks. At 100M URLs the code is 7 characters (62^7 covers 3.5 trillion), and every shorten is its own row, so per-shorten analytics and deletes work.
Run it and verify
uvicorn approaches.approach_c:app --host 0.0.0.0 --port 8000 &
curl -X POST -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/long-path"}' http://localhost:8000/api/shorten
curl -i http://localhost:8000/<the-code> # 302 redirect to the original URL
The redirect returns a 302 and the Location header points at the original URL.
In production you would add a Redis cache in front of the code-to-URL lookup, a CDN so
the redirect itself caches at the edge, and an async click event on every redirect so
analytics never slow the redirect path. Base62-of-id composes cleanly with all three.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Comparing random, hash, and base62-of-id short-code strategies against a 100M-URL workload
- Implementing base62 encoding of an auto-increment id in Python
- Explaining why random codes collide (birthday paradox) and why hashing breaks per-shorten analytics
FAQ
What is the best short code strategy for a URL shortener?
Base62-encoding an auto-increment database id. The id is unique by construction, so there are no collisions and no retry loops, and the code stays around 7 characters even at 100M URLs. Random codes collide and hash-based codes force different users to share one code.
Why do random short codes collide?
By the birthday paradox, a 6-character code from a 62-character alphabet gives about 56 billion combinations but you expect your first collision near 240,000 URLs, not 56 billion. Past that point every insert runs a retry loop to find a free code, which gets slower and eventually fails.
Why not hash the URL to make the short code?
Hashing is deterministic, so the same URL always produces the same code and two different users shortening the same link share one row. That kills per-shorten click analytics, prevents one user from deleting their link without affecting others, and stops you from pointing two users at different destinations.
How long is the short code at 100 million URLs?
Seven characters. Base62 raised to the 7th power is about 3.5 trillion, so a 7-character code comfortably covers 100M URLs with room to spare, while still being short enough to type and share.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →