Designing a URL Shortener: A System Design Walkthrough
A practical, senior-engineer walkthrough of designing a URL shortener: key generation, the read-heavy data model, caching, and the scaling decisions interviewers actually probe.
The URL shortener is the "hello world" of system design interviews, which is exactly why it's dangerous. Everyone knows the shape of the answer, so the bar is high. You get marked on the decisions, not the diagram. Let's walk it like a senior engineer would.
Pin the requirements first
Never start drawing before you size the problem. Ask, then state your assumptions out loud:
- Functional: shorten a long URL, redirect a short code to the original, optional custom aliases and expiry.
- Scale: assume 100M new links per month and a 100:1 read-to-write ratio. That's roughly 40 writes/sec and 4,000 reads/sec — a heavily read-dominated system.
- Non-functional: redirects must feel instant (low p99 latency), and the service must stay available even when writes blip.
That read-heavy ratio is the single most important number on the whiteboard. It justifies aggressive caching and a read-optimized store.
The core: generating the short key
A short code is a base62 string (a to z, A to Z, 0 to 9). Seven base62 characters give you about 3.5 trillion combinations — plenty. You have three credible strategies:
- Hash and truncate: hash the long URL, take the first N characters. Simple, but you must handle collisions with a retry loop.
- Counter plus base62 encode: a monotonic counter encoded to base62. No collisions by construction, but a single counter is a bottleneck and leaks volume.
- Distributed ID ranges: hand each app server a block of IDs from a central allocator. This is the production answer — collision-free, no per-write coordination, and it scales horizontally.
Data model and storage
The schema is almost embarrassingly simple: short_code (primary key), long_url, created_at, expires_at, and an owner_id. There are no joins and no relational complexity, so a key-value or wide-column store fits naturally and partitions cleanly on short_code.
This is a textbook case for picking storage by access pattern, not by familiarity. Your lookup is always a single-key read. Range scans don't matter. Optimize for that.
Make reads fast
With 4,000 reads/sec, you serve the hot path from an in-memory cache (Redis or Memcached) keyed by short code. Popular links — a campaign that goes viral — will dominate traffic, and the cache absorbs that beautifully thanks to a natural power-law distribution.
Use a read-through cache: on a miss, load from the database and populate. Set a TTL so expired links fall out on their own. A CDN in front can even short-circuit the most popular redirects at the edge.
On the redirect itself: prefer a 302 if you want to keep counting clicks and retain control; a 301 is cacheable by browsers and saves you load but blinds your analytics. Know the trade and pick deliberately.
Where interviews go deep
The diagram earns you a passing grade. These follow-ups earn the offer:
- Custom aliases: check uniqueness on write; reject or suffix on collision.
- Analytics: don't write click counts synchronously on the read path. Emit an event to a queue and aggregate asynchronously.
- Abuse: rate-limit creation and screen destinations against a malware list. This is where a clean rate limiter earns its keep.
- Expiry: a background job sweeps expired rows; the TTL handles the cache.
If you want the underlying primitives to feel automatic, drill the System Design lessons and the supporting Algorithms track — encoding, hashing, and partitioning all show up here.
Your move
Reading a walkthrough is not the same as defending one under pressure. Spin up a design round in the arena and see whether your URL shortener survives an AI interviewer at your tier — start from the roadmaps and find out if you can still beat the machine.