The whole interview hinges on one decision: how you generate the short key. Get that right and justify it, absorb the read-heavy scale with a layered cache, and you have passed. Everything else — the API, the storage, the redirect — follows a standard template. Below is the 45-minute walkthrough I use to teach this problem, now with the diagrams I draw on the whiteboard and a couple of interactive widgets so you can feel the mechanics.
A URL shortener (think Bitly or TinyURL) maps a long URL to a short one and redirects on lookup. It is deceptively simple, which is exactly why it is a favorite: interviewers use it to see whether you reason about scale and trade-offs rather than just writing code.
The structure to follow
| Step | Time | Goal |
|---|---|---|
| 1. Clarify requirements | ~5 min | Pin functional + non-functional scope |
| 2. Estimate scale | ~5 min | Justify storage, QPS, read/write ratio |
| 3. API design | ~3 min | Two endpoints, nothing fancy |
| 4. Data model | ~4 min | Pick the store and the schema |
| 5. Core algorithm | ~10 min | How you generate the short key — the crux |
| 6. Scale it | ~10 min | The read-path progression + durability |
| 7. Trade-offs | ~5 min | Name what you would change at 10× |
1. Requirements
Functional: shorten a long URL into a short URL; redirect a short URL to the original; optional custom aliases; optional expiration.
Non-functional: highly available (a dead redirect is a dead link), low-latency redirects (under 100 ms at p99), and read-heavy — reads dominate writes by about 100:1.
Name the read:write ratio out loud
That 100:1 ratio is the single most important sentence in the interview. It drives the whole design — caching strategy, replication topology, even the choice between a 301 and a 302 redirect. Say it in the first two minutes and keep returning to it.
2. Back-of-the-envelope
Assume 100M new URLs per month.
- Writes: 100M / (30 × 86,400) ≈ ~40 writes/sec.
- Reads at 100:1 ≈ ~4,000 reads/sec on average — but traffic is spiky. Apply a peak factor of ~10× and a single viral link can push one key to tens of thousands of reads/sec on its own, so design the read path for ~40,000+ reads/sec at peak.
- Storage: 100M/month × 12 × 5 years × ~500 bytes/record ≈ ~3 TB over 5 years.
- Bandwidth: a redirect response is tiny — mostly the
Locationheader — so even at peak~40k reads/sec × ~500 bytes ≈ ~20 MB/sof egress, and ingress is smaller still. Bandwidth, like storage, is a non-issue. - Cache memory (the 80/20 rule): reads follow a power law, so you don't cache 6 billion mappings — you cache the working set. A small slice of keys serves ~80% of the traffic, so caching even ~100M of the most-recently-active mappings at ~500 bytes is ~50 GB — one replicated Redis node, and exactly what delivers the 90%+ hit rate the read path leans on. Size it to ~70% memory utilization so a spike can't evict the hot set.
Three terabytes is trivial for any modern store, ~40 writes/sec is nothing, and the bandwidth and cache both fit on a single node — so storage, writes, and bandwidth are never the constraint. Read latency and availability are. Finding the real bottleneck before you start drawing boxes is exactly the signal interviewers are grading.
3. API
Two endpoints carry the whole product.
POST /shorten
Body: { "longUrl": "...", "customAlias": "...", "expiry": "..." }
200: { "shortUrl": "https://algoengineer.io/aB3xZ9" }
GET /:shortKey
301 / 302 Location: <original long URL>
DELETE /:shortKey // owner-only; soft-delete the mapping
Use 302 (temporary) if you want every hit to come back to you for analytics or you need to honor expiration; use 301 (permanent) if you want browsers and CDNs to cache the redirect and spare your servers. That single choice is a genuine trade-off — name it rather than picking one silently (and see the expiration tension in Section 8).
The writes carry an API key tied to an account, which gives you a quota lever: the real abuse vector is someone scripting millions of junk links to burn your keyspace and budget, so rate-limit creation per key (a natural hook into the rate limiter). Reads stay open and unauthenticated — they're the whole product.
4. Data model
The access pattern is a pure key-value lookup by short_key, so lead with the access pattern and then pick the store — not the other way around. A single table is enough:
CREATE TABLE urls (
short_key VARCHAR(7) PRIMARY KEY,
long_url TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
expires_at TIMESTAMPTZ,
owner_id BIGINT
);
Which store? With ~40 writes/sec, a few TB, and pure primary-key access, almost anything works — so justify the choice rather than name-dropping. A key-value store (DynamoDB) is the natural fit for PK-only access at scale and shards effortlessly; a single Postgres or MySQL instance also handles this load comfortably. Pick what your team operates well and say why.
Partitioning — name the scheme, don't wave at it. Because every lookup is a single-key read, partitioning is easy to get right, but say which scheme and why:
- Range partitioning (keys
a*on one shard,b*on the next) is the answer to avoid: a counter mints base62 keys roughly in order, so you'd pour every new key onto the same shard and create a write hotspot. - Hash partitioning —
hash(short_key) → shard— spreads reads and writes evenly, and since every access is a single-key lookup, any shard answers independently (no range scans, no joins). This is the right default. - Consistent hashing is how you grow: adding or removing a shard remaps only a slice of the keyspace instead of rehashing all 6 billion keys. Run many logical partitions over fewer nodes so rebalancing is a metadata change, not a migration.
The shard key is the short_key itself — the one thing every read already knows.
Here is the shape of the system you are building toward:
Because reads and writes are so asymmetric, a common refinement is to split the stateless tier into a Write Service (mints keys, persists) and a Read Service (redirects, cache-first) so each scales independently — you will run far more Read instances than Write.
5. The crux: generating the short key
This is where the interview is won or lost. Three common approaches:
| Approach | How | Pros | Cons |
|---|---|---|---|
| Hash (e.g., MD5) + truncate | Hash the long URL, take first N chars, base62-encode | Stateless, deterministic | Collisions — need a check-and-retry loop |
| Random key + collision check | Generate random base62 of length 7 | Simple, unguessable | Read-before-write on collision; degrades as space fills |
| Counter + base62 (recommended) | Auto-increment id → base62-encode it | No collisions, short keys, O(1) | Keys are sequential/guessable; the counter is a write bottleneck |
Two subtler traps make the hash approach worse than the "collisions" cell lets on: the same long URL hashes to the same key for everyone (so you can't hand two users distinct links to one destination), and URL-encoded variants of the "same" link hash to different keys (?a=1&b=2 and its escaped form look identical to a human but not to the hash). The counter sidesteps both by construction.
Base62 uses a–z, A–Z, and 0–9, so 62⁷ ≈ 3.5 trillion keys fit in just 7 characters — far more than five years of traffic needs (and 7 leaves headroom for custom aliases and churn). Try it: the widget below encodes an auto-increment id into the exact short key a counter-based design would store.
Counter → base62 short key
interactiveShort URL
algoengineer.io/4c92
4 base62 characters address up to 14,776,336 keys — no collisions, no read-before-write.
The counter approach is the cleanest, and it sets up the obvious follow-up: "How do you scale the counter without it becoming a single write bottleneck?" There are two production answers; lead with id ranges.
Option A — id ranges (a ticket service). Stop minting ids one at a time from a central place and instead hand each server a range:
Claim a range
An application server asks a central ticket service for a block of ids and receives, say, 1–1,000,000.
Mint locally
It encodes each new id to base62 on its own — zero coordination, no network call on the hot write path.
Refill in the background
As it nears the end of its block, it asks for the next range (1,000,001–2,000,000) before it runs out, so writes never stall.
Tolerate gaps
If a server dies mid-block, its unused ids are simply lost. That is fine — the keyspace is astronomically large, so gaps cost nothing.
Option B — a batched Redis INCR. A single Redis INCR is atomic and does ~100k+ ops/sec; to avoid a network hop per write, each server INCRBYs a batch of, say, 1,000 and mints locally — the same idea, backed by Redis instead of a bespoke service. Either way, keep a UNIQUE constraint on short_key as a last-resort safety net so a bug can never silently overwrite a mapping.
Range allocation is the part most candidates miss. It is exactly the pattern behind Flickr's ticket server; Twitter's Snowflake is the coordination-free cousin (time-sortable ids minted with no central counter). Making the keys unguessable is a separate concern — the counter-permutation trick in Section 10 is the idea behind Hashids and Instagram-style id obfuscation.
The alternative to name: a key-generation service (KGS)
Instead of minting keys on the fly, a standalone service pre-generates a huge pool of random keys ahead of time and stores them in a table split into used and unused; shortening just hands out the next unused key — no encoding, no collision check at all. Worth naming, because an interviewer trained on the classic answer will ask for it. But it's the alternative, not my default: you're now operating a separate service and a multi-gigabyte pre-generated store, it's a single point of failure that needs a standby, and you must lock the in-memory batch each server hands out so two servers never serve the same key. The counter gives the same collision-free guarantee with none of that — which is why I lead with it. Knowing KGS and why you'd skip it is the senior move.
6. Scaling the read path
Because the system is about 100:1 read-heavy, the read path is where you spend your scaling budget. The two paths could not be more different — one is rare and can afford coordination, the other is constant and must be cheap:
Write vs read traffic
~100:1 readsReads dominate by ~100:1, so the redirect path is cache-first and the write path can afford a coordinating ID service. Optimize the green path; tolerate the blue one.
Build the read path as a progression, and put numbers on why each rung exists:
The read-path ladder (why each layer earns its place)
| Layer | Speed / ceiling | Why you add it |
|---|---|---|
| Indexed DB (B-tree) | O(log n) lookup, but an SSD does ~100k IOPS at ~0.1 ms (a spinning disk seek is ~10 ms) | Correct and simple, but caps out under a 40k+/s spike |
| Redis cache | memory read ~100 ns (~1,000× faster than SSD), millions of ops/sec | Power-law access → 90%+ hit rate keeps redirects under 100 ms |
| CDN edge (301) | answered at a PoP near the user | Most traffic never reaches your origin at all |
Cache aggressively. Put the hottest short_key → long_url mappings in Redis. URL access follows a power law — a handful of links go viral while the long tail is quiet — so a 90%+ hit rate is realistic. Use an LRU eviction policy and a TTL; expect a brief cold-start "warmup" after a cache flush, when more traffic hits the database. Here is the exact read path, cache-first:
Replicate reads. Read replicas absorb the cache misses; writes still go to the primary. Push to the edge. With 301s, CDNs cache the redirect itself, so a large share of traffic is answered at the edge and never reaches your origin.
7. Durability, multi-region & failover
Availability was a hard requirement, so make the design survive a node — and a region — going down:
- Replication + backups. The primary store replicates to followers (the same replicas that serve reads) and takes regular backups; on primary failure, promote a replica. A few seconds of write unavailability is acceptable; lost mappings are not.
- Multi-region with disjoint counter ranges. To run the write path in two regions without coordination, give each region a disjoint id range — Region A mints
0–1B, Region B mints1B–2B— so keys never collide and the regions never talk on the write path. Replicate the URL store cross-region and serve reads from the nearest region. - Counter-service failover. Back the ticket service (or the Redis
INCR) with replication and automatic failover (Redis Sentinel/Cluster). Losing a few counter values on failover is harmless — we need uniqueness, not a gapless sequence.
8. Custom aliases & expiration
These two "optional" features quietly complicate the clean counter scheme, so address them explicitly:
- Custom aliases (
/my-brand) bypass the counter, so they need their own uniqueness check on write (reject if taken) and share the same keyspace as generated keys — reserve a prefix or check both on insert so a custom alias can never collide with a future generated key. - Expiration is where 301 vs 302 bites. Purge lazily, not with a constant full-table scan (which pressures the DB for nothing): when a lookup lands on an expired row, return 410 Gone and drop it then, and run the actual reclaim as a lightweight sweep at low-traffic hours (or lean on a KV store's native TTL). If you ever recycle keys — rarely worth it given the keyspace — return the freed key to the pool. The catch: if you served 301s, browsers and CDNs cached the redirect and will keep honoring it long after you delete it. That stale-redirect problem is a big reason production shorteners that support expiry or deletion default to 302 — you cannot un-cache a 301 you have already handed out.
9. Failure modes interviewers probe
The deep-dive is where senior candidates pull ahead. Have these four ready before you are asked.
Cache stampede on a viral link
When a hot key expires, thousands of concurrent requests all miss the cache and hammer the database at once. Defend with a short randomized TTL jitter, request coalescing (single-flight) so only one request refills the key, or by serving slightly stale values while one background refresh runs.
Counter / ticket service outage
If the id service is down, no server can refill its range. Keep ranges large enough to ride out a brief outage, replicate the ticket service, and degrade gracefully — writes can queue or briefly fail, but reads must keep serving since they never touch the counter.
Hot key and the celebrity link
One link going viral can overwhelm a single cache node or shard. Replicate hot keys across cache nodes, or let the CDN absorb the spike with 301s, so no single node is a choke point.
Replication lag
A URL created on the primary may not be on a read replica yet, so a just-created short link can 404 for a few seconds. Read-your-writes from the primary (or the cache) for the creating user, and accept eventual consistency for everyone else.
10. Trade-offs, follow-ups & what to say at each level
Trade-offs to name unprompted: 301 vs 302 (CDN caching and lower load versus analytics fidelity and the ability to expire); sequential keys are guessable (fine for public links, a problem for sensitive ones — permute the counter); the multi-region consistency you accept (eventual, cross-region).
Follow-ups a senior interviewer asks next: an analytics pipeline (fire async click events to a stream → aggregate, instead of counting inline), abuse/spam (rate-limit creation, scan submitted URLs for malware), private / ACL'd links (the same short key, gated by a permission table mapping users → the keys they may resolve, returning 401/404 to everyone else), and custom domains per customer.
What to say at each level. Mid: counter + base62, a cache, 301/302. Senior: id-range scaling, hash partitioning of the store, the cache → replica → CDN progression with the read:write asymmetry, and the failure modes. Staff+: multi-region disjoint counter ranges + failover, consistent-hashing resharding, the analytics pipeline, the security of guessable keys, private-link ACLs, and the 301-vs-expiration consistency problem — all unprompted.
Why this problem keeps showing up
It is small enough to finish in 45 minutes but rich enough to expose whether you can estimate scale, identify the real bottleneck (the counter and the read path, not storage), and discuss trade-offs without prompting. Master this template and "design Instagram," "design a pastebin," and "design a distributed ID generator" all become variations on the same moves. For the method behind it, start with the system design interview framework; then try the read-and-write-heavy Instagram feed or the connection-heavy WhatsApp.
Amit Singh is a Senior SDE at Amazon, a Claude Certified Architect, and the instructor at AlgoEngineer. He has run this exact walkthrough in dozens of mock interviews — book one in the System Design course.