URL Shortener System Design
A URL shortener maps a long URL to a short code and redirects on lookup. The design hinges on one decision - how you generate the short code - plus the data model and the redirect path. Here's the whole thing.
The requirements
- Shorten:
POST /with a long URL returns a short code (e.g.aZ3x9). - Redirect:
GET /<code>301-redirects to the original URL. - Codes should be short (~6-7 chars), unique, and hard to guess is a bonus.
- Reads (redirects) hugely outnumber writes - optimize for fast lookups.
The key decision: generating the short code
| Scheme | Pros | Cons |
|---|---|---|
| Base62 of an auto-increment id | Dense, guaranteed unique, ~6 chars at scale, no collision checks | Sequential - leaks growth rate, codes are guessable |
Hash of the URL (md5(url)[:n]) |
Same URL -> same code (free dedup) | Collisions at scale need handling |
| Random string | Unguessable | Must check the DB for collisions on every insert |
Base62 of the row id is the usual default: store the row, take its integer id, encode it base62. Unique by construction - no collision check needed.
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def base62(n: int) -> str:
if n == 0:
return ALPHABET[0]
s = []
while n:
n, r = divmod(n, 62)
s.append(ALPHABET[r])
return "".join(reversed(s))
The data model
One table is enough to start:
CREATE TABLE urls (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- the id we base62-encode
url TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Insert the URL, read back the id, return base62(id) as the code. On redirect,
decode the code back to the id (or just store the code and index it) and look up the URL.
The redirect flow
@app.get("/<code>")
def redirect_code(code):
row = db.execute("SELECT url FROM urls WHERE code = ?", (code,)).fetchone()
if not row:
abort(404)
return redirect(row["url"], code=301) # 301 = permanent, cacheable
Use 301 so browsers and CDNs cache the redirect - that's what makes reads cheap.
How it scales
- Reads dominate -> put a cache (Redis) in front of the code->URL lookup; most redirects never touch the database.
- Distributed ids -> a single auto-increment is a bottleneck across many nodes; use a key-generation service or a sharded/range-allocated id scheme so each node mints ids without coordinating on every write.
- Storage -> the table is tiny per row; the real cost is read throughput, which the cache + CDN absorb.
- Custom aliases & analytics -> add a
codecolumn with a uniqueness constraint for vanity URLs; log redirects async for click stats.
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
- Choosing a short-code scheme and implementing base62 encoding
- Designing the data model + redirect endpoint
- Reasoning about caching + distributed ids for scale
FAQ
How does a URL shortener generate the short code?
The common approach is base62-encoding an auto-increment row id - unique by construction, ~6 chars at scale, no collision checks. Alternatives are hashing the URL (free dedup, but collisions) or random strings (unguessable, but you must check for collisions).
Why use a 301 redirect for a URL shortener?
301 (permanent) lets browsers and CDNs cache the redirect, so most requests never hit your service - which is what keeps a read-heavy shortener fast and cheap. Use 302 only if you need to count every click.
How do you scale a URL shortener?
Cache the code-to-URL lookup (reads vastly outnumber writes), serve redirects through a CDN, and replace the single auto-increment id with a distributed id scheme so writes don't bottleneck on one counter.
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 →