Back to Blog
System DesignInterview TipsDistributed Systems

Design a URL Shortener — System Design Interview Walkthrough

Amit Singh

Amit Singh

Author

June 25, 2026
18 min read

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

StepTimeGoal
1. Clarify requirements~5 minPin functional + non-functional scope
2. Estimate scale~5 minJustify storage, QPS, read/write ratio
3. API design~3 minTwo endpoints, nothing fancy
4. Data model~4 minPick the store and the schema
5. Core algorithm~10 minHow you generate the short key — the crux
6. Scale it~10 minThe read-path progression + durability
7. Trade-offs~5 minName 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 Location header — so even at peak ~40k reads/sec × ~500 bytes ≈ ~20 MB/s of 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 partitioninghash(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:

High-level architecture
Storage tierClientLoad balancerAPI servicestatelessTicket serviceid rangesRedis cachehot keysURL store+ read replicasrequestnext rangereadwrite / miss
Clients hit a load balancer fronting a stateless API tier. Writes draw an id range from the ticket service and persist to the store; reads are served cache-first from Redis, backed by read replicas.

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:

ApproachHowProsCons
Hash (e.g., MD5) + truncateHash the long URL, take first N chars, base62-encodeStateless, deterministicCollisions — need a check-and-retry loop
Random key + collision checkGenerate random base62 of length 7Simple, unguessableRead-before-write on collision; degrades as space fills
Counter + base62 (recommended)Auto-increment id → base62-encode itNo 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

interactive

Short 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:

  1. Claim a range

    An application server asks a central ticket service for a block of ids and receives, say, 1–1,000,000.

  2. Mint locally

    It encodes each new id to base62 on its own — zero coordination, no network call on the hot write path.

  3. 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.

  4. 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 reads
Write — POST /shorten (rare)
ClientAPIID serviceDB write
Read — GET /{key} → 301 (constant)
ClientAPIRedis cache301 redirect

Reads 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)

LayerSpeed / ceilingWhy 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 cachememory read ~100 ns (~1,000× faster than SSD), millions of ops/secPower-law access → 90%+ hit rate keeps redirects under 100 ms
CDN edge (301)answered at a PoP near the userMost 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:

The redirect path
GET /:keylookuphit → long URLmiss → read replica → backfill cache with a TTL301 / 302ClientAPIRedis cacheURL store
Check Redis first; on a hit, return the redirect immediately. On a miss, fall back to a read replica, backfill the cache with a TTL, then redirect.

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 mints 1B–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.

Design a URL shortener in a system design interview

The seven-step structure for walking through a URL shortener design in about 45 minutes.

  1. 1

    Clarify requirements

    Separate functional (shorten, redirect, optional custom alias and expiry) from non-functional (highly available, low-latency redirects, read-heavy at about 100:1).

  2. 2

    Estimate scale

    Back-of-the-envelope writes, reads at the 100:1 ratio with a peak factor, and five-year storage. Conclude that read latency and availability — not storage — are the constraints.

  3. 3

    Design the API

    Two endpoints: POST /shorten to create a short URL and GET on the short key to redirect.

  4. 4

    Choose the data model

    A key-value lookup by short key. A KV store or a well-indexed relational table both work; lead with the access pattern.

  5. 5

    Generate the short key

    The crux. Compare hashing, random keys, and a counter plus base62; recommend the counter and explain how to scale it with id ranges or a batched Redis INCR.

  6. 6

    Scale the read path

    Layer indexing, a Redis cache, read replicas, and CDN-edge 301s, with the IOPS/latency numbers that justify each rung.

  7. 7

    Name the trade-offs

    301 vs 302 and expiry, guessable keys, custom aliases, multi-region, and durability — surface them before the interviewer asks.

Frequently asked questions

Should a URL shortener return a 301 or a 302 redirect?
It is a real trade-off. A 301 (permanent) lets browsers and CDNs cache the redirect, which offloads huge amounts of traffic from your servers — but you lose per-click analytics because repeat visits never reach you, and a cached 301 is hard to ever change or expire. A 302 (temporary) routes every hit back through your service so you can count clicks and honor expiration/deletion, at the cost of carrying all that traffic yourself. Lead with 302 if analytics or expiry matter; otherwise 301 scales better.
Why use a counter plus base62 instead of hashing the URL?
Hashing (e.g. MD5 then truncate) is stateless but produces collisions, so you need a check-and-retry loop that gets slower as the keyspace fills. An auto-increment counter encoded to base62 is collision-free by construction, produces the shortest possible keys, and is O(1). Its one weakness — a central counter is a write bottleneck — is solved by handing each server a pre-allocated range of ids, or by batching a Redis INCR.
How do you scale the counter without a single bottleneck?
Use a ticket/ID service that hands each application server a block of ids (for example server A gets 1–1,000,000, server B gets 1,000,001–2,000,000). Each server mints keys locally with zero coordination and asks for a new range only when it runs low. This removes the per-write hot spot entirely; ids from a crashed server are simply lost, which is harmless given the enormous keyspace. Across regions, give each region a disjoint range so they never coordinate.
How do you make redirects fast at scale?
Layer the read path: an indexed database lookup is O(log n) but an SSD caps out around 100k IOPS, so put the hot mappings in a Redis cache (memory is ~1,000× faster than disk and a power-law access pattern gives a 90%+ hit rate), add read replicas to spread the rest, and push 301-cacheable redirects to the CDN edge so most traffic is answered near the user and never reaches your origin.
How much storage does a URL shortener actually need?
Very little. At 100 million new URLs per month and roughly 500 bytes per record, five years of data is about 3 TB — trivial for any modern database. Storage is never the constraint for this system; read latency and availability are. Say that explicitly so the interviewer knows you found the real bottleneck.
Are sequential short keys a security problem?
They can be. A counter produces guessable, enumerable keys, so anyone can walk /1, /2, /3 and discover links. That is fine for public links but a problem for anything sensitive. Mitigate by encrypting or permuting the counter (for example with a Feistel network or a multiply-by-a-large-coprime trick) so the output looks random while staying collision-free, and rate-limit creation to slow scrapers.
How do you partition a URL shortener’s database?
Hash-partition by the short key: hash(short_key) maps each mapping to one of N shards, and because every lookup is a single-key read, any shard answers independently with no range scans or joins. Avoid range partitioning (keys a*, b*, …) because a counter mints base62 keys roughly in order, so you would pour every new key onto one shard and create a write hotspot. To grow, use consistent hashing so adding or removing a shard remaps only a slice of the keyspace instead of rehashing billions of keys, and run many logical partitions over fewer physical nodes so rebalancing is a metadata change rather than a migration.

Ready to Ace Your Interviews?

Live, cohort-based interview prep taught by a working FAANG engineer — weekly mock interviews, lifetime access, and a 7-day money-back guarantee.