"Design TinyURL" is the classic warm-up question in almost every HLD loop — deceptively simple until you ask how many redirects a popular link needs to survive per second. This lesson puts nearly every earlier lesson to work on one concrete system, start to finish.
Requirements and capacity estimation
Functional: given a long URL, return a short one; given the short one, redirect to the original. Non-functional: assume 100 million new links created per month, each read (redirected) 100 times on average over its lifetime.
Writes ≈ 100,000,000 / (30 × 86,400) ≈ 39 writes/sec average Reads ≈ (100,000,000 × 100) / (30 × 86,400) ≈ 3,858 reads/sec average → roughly 100x more reads than writes: this system is read-heavy.
That read-heavy conclusion is not a throwaway detail — it decides almost everything about the architecture below.
Generating short codes
Most production shorteners base-62 encode (a–z, A–Z, 0–9) an auto-incrementing counter, rather than generating a random string and checking for collisions. Encoding a unique, ever-increasing counter gives a short code that is unique BY CONSTRUCTION — zero collision risk, no database check-and-retry loop needed, unlike random generation which needs an extra lookup (and possible retry) every single time to confirm no collision happened.
Database schema and the read path
A minimal schema: links(short_code PRIMARY KEY, long_url, created_at, expires_at). The redirect path (GET /:short_code) is the single most-hit endpoint in the entire system by a huge margin — it deserves a cache in front of the database, following the cache-aside pattern from lesson 5.
Caching the hot short-code → URL mappings
Custom aliases
Letting users request a custom alias (bit.ly/my-brand) breaks the "unique by construction" guarantee — the write path now needs an explicit uniqueness check. The safe way to do this under concurrent requests for the same popular alias is a UNIQUE constraint on the alias column: both attempts try to INSERT, the database allows exactly one, and the other fails with a constraint violation the application catches and reports as "alias already taken." A check-then-insert from application code alone has a race window; the database constraint closes it.
Scaling the read path: the redirect
If one specific link goes viral and receives 50,000 redirects a second, that traffic should be served almost entirely from the cache — a single hot key served from Redis can comfortably absorb far higher read QPS than a disk-backed database would tolerate for the same key. This is the caching lesson's core promise, realized in the clearest possible real-world example.
The final architecture
Client → load balancer → stateless app servers → cache (Redis, for the hot redirect path) → database (for the authoritative short_code → long_url mapping and for cache misses). Writes are infrequent enough (≈39/sec) that a single primary database, without sharding, handles them comfortably; reads lean almost entirely on the cache. Nothing here needed a message queue, a CDN, or sharding — the numbers never demanded them, which is itself worth saying out loud in an interview.
Common mistakes
- Generating random codes and checking for collisions instead of encoding a unique counter.
- Forgetting the read-heavy conclusion, and under-investing in caching the redirect path.
- Handling custom-alias uniqueness with a check-then-insert instead of a database-level unique constraint.
- Reaching for sharding or a message queue this system's actual numbers never justify.
Quick recap
| Concept | One-liner |
|---|---|
| Base-62 encoding | Unique by construction — no collision checks needed. |
| Read-heavy | ~100x more reads than writes — caching is the key lever. |
| Custom aliases | Need a DB unique constraint, not an app-level check-then-insert. |
| Viral link | Served almost entirely from cache, protecting the database. |
Practice Zone
Five MCQs, then two applied questions.
Why do most URL shorteners use base-62 encoding (a-z, A-Z, 0-9) of an auto-incrementing id instead of generating a random string and checking for collisions?
Asked in


A URL shortener is described as extremely 'read-heavy' — why, and what does that imply for the architecture?
Asked in


A URL shortener lets users request a custom alias (e.g. bit.ly/my-brand) instead of an auto-generated code. What extra design consideration does this add?
Asked in

Why might a URL shortener want links to expire after a configurable time (or after inactivity), even though storage is cheap?
Asked in

One shortened link suddenly goes viral and receives 50,000 redirect requests per second, far more than any single database replica can serve. What is the most direct architectural fix?
Asked in


Estimate: if the service creates 100 million new short links per month, and each link is read (redirected) 100 times on average over its lifetime, what is the approximate write QPS and read QPS the system needs to sustain (assume traffic is roughly even across a 30-day month)?
Asked in


Two users simultaneously try to claim the same custom alias 'my-link' at nearly the same instant. Design a way to guarantee only one of them succeeds, without a noticeable slowdown for the common case (no collision).
Asked in

FAQ
Why base-62 instead of base-64?
Base-64 includes characters like '+' and '/' that need URL-encoding to be used safely in a URL path; base-62 (letters and digits only) avoids that problem entirely.
Should link expiry be a required feature?
Not necessarily — it depends on stated requirements, but it's worth mentioning as an option: it bounds index growth over time and lets short codes be recycled, though it isn't strictly necessary at modest scale.
Does this system need a CDN?
Not typically — the redirect response itself is small and depends on a database lookup (even if cached), which a CDN alone doesn't replace; caching (Redis) is the more directly applicable lesson here.


