InfoWok
Intermediate

Design a URL Shortener: Where Every Decision Comes From the Read Path (2026)

The classic warm-up problem, walked end to end in 45 minutes. Short codes, a hundred reads for every write, 301 versus 302, and the 2026 twist of abuse, with diagrams that grow as the design does.

SK
Sukhveer Kaur
Published October 19, 2026
11 min read
Dark terminal-style card titled URL Shortener, problem 1 of the system design interview course, with the line design it in 45 minutes, starting from the read pathSoftware Architecture
DESIGN A URL SHORTENER
On this page +

“Design a URL shortener.” It sounds like a warm-up, and that is the trap. The candidate who treats it as easy draws a database, a hash function and an arrow, and is done in ten minutes. Then the interviewer asks what happens at twelve thousand clicks a second, why a cached link still points at a phishing page, and whether codes can be guessed in order. The remaining thirty-five minutes go badly.

This problem is a favourite because it is small enough to finish and deep enough to separate levels. Every important decision in it comes from one fact: people click links far more often than they create them. This lesson walks the full 45-minute round with the course framework, and the design grows one diagram at a time.

🟡 Intermediate⏱️ 35 min
✅ Before you start
📌 Remember from lesson 2

For 100 million new links a month with 100 reads per write, what did the estimate decide? Reads shape the design, so a cache earns its place, and a six-character code is enough.

🎯 Key takeaways
  • Reads outnumber writes about a hundred to one, so the read path decides the architecture.
  • Short codes come from a unique number encoded in base62. Hand out numbers in blocks, and scramble them if codes must not be guessable.
  • Return 302, not 301, unless you never need click counts, expiry or takedowns.
  • A cache in front of the database serves most redirects; the database is the source of truth.
  • In 2026, abuse is part of the design: rate-limit creation and check destinations.
After this lesson you can

Design a URL shortener end to end in 45 minutes: pick a code-generation scheme and defend it, size and cache the read path, choose the redirect status code, and handle expiry and abuse.

Step 1: Pin Down the Problem#

Start with three questions out loud: what must it do, what will it not do, and how big is it? Write the answers where the interviewer can see them.

Functional requirements:

  1. Users can submit a long URL and get a short link back.
  2. Anyone opening the short link is redirected to the long URL.
  3. Users can choose a custom alias and an expiry time (optional).

Out of scope (say these out loud so the interviewer can object):

  • Accounts and login, editing a link after creation, analytics dashboards.

Non-functional requirements, each with a number. These are assumptions you announce, not facts:

  • Redirects are fast: under 100 ms on the server for most requests.
  • Redirects are highly available. A broken short link breaks every page that contains it.
  • Codes are unique, and a code never silently points somewhere new.
  • Scale: 100 million new links a month, 100 reads for every write.

The last line is the one to underline. It says this is a read-heavy system, and almost everything that follows is about serving reads.

Step 2: Entities and API#

text
ShortLink  code, long_url, created_at,
           expires_at, owner_id
 
POST /links   { long_url, alias?, expires_at? }
              -> 201 { code, short_url }
GET  /{code}  -> 302, Location: long_url
              -> 404 unknown · 410 expired

One entity and two endpoints. The redirect status code in that sketch is a decision, not a detail, and step 5 comes back to it.

Step 3: A Design That Works#

Walk the endpoints one at a time. POST /links needs something to create a unique code and somewhere to store it. GET /{code} needs to look the code up and return a redirect. That is a service and a database, nothing more.

One detail belongs here, not in a deep dive: validate and normalise the long URL before storing it. The standard URI rules (RFC 3986) give you most of it:

  • accept only http and https;
  • lowercase the scheme and host, which are case-insensitive;
  • drop a default port such as :80, and treat an empty path as /.

HTTP://Example.com:80 and http://example.com/ are then stored as one destination, and the abuse checks later see the real host.

This design is correct. It is also where many candidates stop. Before reading on, find its weak point.

💡 Predict first

At 12,000 redirects a second at peak, which part of this picture breaks first, and why? Commit to an answer, then check it against step 4.

Step 4: Estimate What Matters#

These are the same assumptions as lesson 2, so the numbers carry over.

text
Writes   100M / 2.5M s     ≈ 40 /s       peak ≈ 120 /s
Reads    40 x 100          ≈ 4,000 /s    peak ≈ 12,000 /s
Storage  100M x 500 B x 60 months ≈ 3 TB
         x 3 replicas      ≈ 9 TB
IDs      100M x 60 months  ≈ 6 billion
         62^6 ≈ 56.8 billion   62^7 ≈ 3.52 trillion

Each line ends in a decision. Writes at 120 a second are light, so code generation does not need heroics. Reads at 12,000 a second all hit the same lookup, so the database read path is what breaks first: put a cache in front of it. Storage of 9 TB with replicas is real but not urgent. Six characters cover the IDs; seven leave room to scramble them.

Step 5: Deep Dives#

Three questions decide this design. Pick them in this order unless the interviewer steers you elsewhere.

How Do We Generate Unique Short Codes?#

A short code is a number written in base62. The question is where the number comes from.

Works · Hash the long URLcorrect, simplest

Hash the long URL and keep the first seven base62 characters. It needs no shared state, and the same URL always gets the same code.

⚖️ Trade-off

Different URLs can collide on seven characters, so every write must check the database and retry with a salt. Near six billion stored links, about one insert in 600 collides (6 billion ÷ 3.52 trillion).

Two users shortening the same URL also share a code, so one user’s expiry affects the other.

Scales · One global counter, encoded in base62handles the load

Keep a counter in a store with an atomic increment, such as Redis INCR, which is O(1) and works on 64-bit integers (Redis). Each new link takes the next number and encodes it. No collisions, no retries.

⚖️ Trade-offEvery write now calls one counter, which is a single point of failure on the write path. And the codes come out in order: aaaab, aaaac. Anyone can walk the whole keyspace and read every link people assumed was private.

Survives · Number blocks per server, scrambled before encodinghandles failure and growth

Each URL service instance reserves a block of numbers from the counter, say 1,000 at a time, and hands them out from memory. Before encoding, pass the number through a keyed, reversible scramble over the 7-character space, so neighbouring numbers produce unrelated codes.

⚖️ Trade-offA server that crashes wastes the rest of its block, which is harmless in a space of 3.5 trillion codes. The counter is touched once per thousand writes, so it can fail for minutes without stopping link creation. The scramble key becomes a secret you must protect and never change.

Custom aliases skip the counter: check the alias is free with a conditional insert, and reserve a prefix or minimum length so aliases cannot collide with generated codes.

How Do We Serve 12,000 Redirects a Second?#

Concept in use: Caching strategies

The same small set of popular links is read again and again and almost never changes. That is the textbook case for cache-aside.

Works · Look up the database on every clickcorrect, simplest

The code is the primary key, so each redirect is one indexed lookup.

⚖️ Trade-offEvery click is a database round trip. At peak, the database spends its capacity answering the same popular codes repeatedly.

Scales · Cache-aside in memoryhandles the load

Look the code up in an in-memory cache first; on a miss, read the database and store the result with a time-to-live. Popular links stay hot, and the database sees mostly misses.

⚖️ Trade-offA cached entry can outlive a change. Cap the time-to-live at the link’s remaining lifetime, and delete the entry when a link is taken down.

Survives · Cache plus replicas across zoneshandles failure and growth

Run the cache and read replicas of the database in more than one availability zone, with the primary handling writes. Losing a zone loses capacity, not links.

⚖️ Trade-offReplicas lag the primary slightly, so a link read in the first moments after creation may miss. Read your own writes from the primary, or fall back to it on a miss.

🔑 One step further: answer at the edge

At very high or very global traffic, move the redirect itself to a CDN. Edge platforms run small functions close to users that can return a redirect directly, for example a Cloudflare Worker calling Response.redirect (Cloudflare). Popular codes then never reach your servers. The catch is control: a 302 is not cached unless you send explicit cache headers (RFC 9110), so give edge entries a short max-age and purge them on takedown. See CDNs and object storage.

Which Database?#

Concept in use: SQL vs NoSQL

The access pattern is one key, one value. A relational database with the code as primary key handles this scale; a key-value store is the natural next step once you partition by code.

301 or 302? The Status Code Is a Design Decision#

The redirect status code decides who remembers the mapping.

301 / 308 (permanent)302 / 307 (temporary)
Browser may cache ityes, by defaultno, unless you say so
Later clicks reach youmaybe notyes
Click counts, expiry, takedownlost for cached clientswork
Load on your serverslowerevery click

The difference comes straight from the HTTP standard. A 301 response “is heuristically cacheable”, so a browser may reuse it without asking again, while 302 and 307 are not unless explicit cache headers allow it (RFC 9110). The same section notes that browsers may turn a POST into a GET on 301 and 302, which 307 and 308 prevent; for links opened with GET that does not matter.

Default to 302. You keep every click, links can expire, and a link you take down stops working for everyone. Choose 301 only for links that are truly permanent and where you never need those controls.

Return 410 Gone rather than 404. It tells clients the link was removed on purpose and is likely gone for good, while 404 is meant for cases where the server cannot say (RFC 9110). Then:

  • Check expires_at on every read, so a link stops at the right moment even before cleanup runs.
  • Cap cache entries at the link’s remaining life.
  • Delete expired rows in small batches with a background job in quiet hours, so cleanup never competes with redirects.
🔑 Takedown uses the same three layers

Each layer has to forget the link on its own, which is why the 302 matters: with it, no browser holds a copy you cannot reach.

Two things change when the system grows past the estimate.

A link goes viral. Every request now asks for the same key, and one cache node takes the whole load. This is a hot key. Copy hot entries to several cache nodes or keep them in each service’s local memory for a few seconds, and let the edge absorb the rest.

The service spans regions. Three changes keep writes local and reads fast:

  • Split read and write services and scale them separately. Reads are stateless and grow far faster.
  • Give each region its own number range, so regions never coordinate on writes.
  • Run the counter store with automatic failover. Redis Sentinel, for example, promotes a replica when the primary fails (Redis). A failover may skip part of a block, which is harmless, and a UNIQUE constraint on code stays the final safety net.

New links copy to the other regions asynchronously. A click that lands in another region before its link arrives can still be served: reverse the scramble to get the number, and the range it falls in names the home region to ask.

Interviewers often ask for alternatives to the counter. Know these four:

SchemeHow it worksWhy or why not here
Number blocks (this design)counter hands out rangesshort codes, one rare dependency
Snowflake-style IDs41-bit time + 10-bit machine + 12-bit sequence, no coordination (Twitter)about 11 base62 characters: too long for a short link
Pre-generated key poola separate service fills a pool of unused random codesrandom codes, but one more service to run
Hash + Bloom filtera Bloom filter answers “definitely unused” without a database read (Wikipedia)cuts collision checks; false positives cost a retry

The 2026 Twist: Abuse#

A shortener hides the destination, which makes it useful to anyone sending a phishing link. Ask early whether abuse is in scope; at senior level and above, raise it yourself. The plan:

  • Rate-limit link creation per user and per IP address. See rate limiting.
  • Check the destination when the link is created, using a URL-reputation service. Google’s Web Risk, for example, checks URLs against lists of phishing, malware and unwanted-software sites, and warns that it can miss risky sites or flag safe ones (Google Cloud).
  • Re-check popular links later, because a page can turn malicious after the link is made.
  • Make takedowns instant: mark the link disabled, delete it from the cache, and rely on 302 so browsers ask again.

The Final Design in One Picture#

Follow one click: the edge answers it if the code is hot; otherwise the gateway passes GET /{code} to the read service, which finds the code in the cache and returns a 302 with the long URL. A cache miss reads a database replica and refills the cache.

A new link goes to the write service: its URL is validated and normalised, it takes a number from the server’s local block, is scrambled and encoded, has its destination checked, and is written to the primary.

What Each Level Should Show#

LevelExpected in the room
Mid-levelA working design, base62 codes, a cache for reads. The interviewer may lead the deep dives.
SeniorDrives the deep dives, sizes the read path, compares code-generation options, argues 302 over 301.
Staff+Raises abuse and takedown, guessable codes, hot keys, per-region number ranges, counter failover, edge serving and its cost, and what to build first.

Red Flags and Green Flags#

✗ Red flag

“I’ll take an MD5 of the URL.” Then no word about collisions, guessable codes or how two users with the same URL are kept apart.

✓ Green flag

“Writes are about 40 a second and reads a hundred times that, so I’ll keep code generation simple and spend the deep dive on the read path.”

⚠️ The most common way to lose this round

Spending twenty minutes on the hash function. Code generation is one deep dive of three, and the read path is where the load is. Pick a scheme, state its trade-off, and move on.

Try It: Add Click Analytics#

Exercise · Count clicks without slowing redirects15 min

The product team now wants a daily click count per link. Redirects must stay as fast as before. Sketch:

  1. Where the click is recorded, and why not in the database on the redirect path.
  2. How counts reach a daily total.
  3. One trade-off of your choice.
Hint

At 12,000 redirects a second, a database write per click doubles the work on the hot path. What can the redirect hand off instead of doing itself?

One reasonable answer
text
GET /{code} -> 302 (unchanged)
            -> emit click event (code, time) to a log
               or queue, fire and forget
Consumer    -> aggregate per (code, day) in batches
            -> write daily totals to an analytics store

The redirect only emits an event and returns; counting happens off the hot path. A durable log such as Kafka lets the consumer fall behind and catch up without losing clicks. Trade-off: counts are minutes behind real time, and you must decide whether a lost event matters for billing or only for charts. See event-driven architecture and Kafka vs RabbitMQ.

Test Yourself#

In an interview

Expect these follow-ups once the design works: “Two users shorten the same URL; do they get the same code?”, “A link points to malware; how fast can you stop it?”, and “Your counter store goes down; what still works?” Each one maps to a deep dive above, so answer by pointing at the part of the diagram that handles it.

Check yourself4 questions · answer, then open
1.Why does a sequential counter make codes guessable, and how do you fix it without collisions?

Consecutive numbers encode to consecutive codes, so anyone can enumerate links. Pass each number through a keyed, reversible scramble before encoding: unique numbers still give unique codes, but neighbours look unrelated.

2.Traffic grows tenfold to 120,000 redirects a second at peak. What changes?

Writes are still modest, so code generation is unchanged. The cache takes most of the growth: more cache capacity and more replicas spread across zones. Watch the cache hit rate, because every miss now costs ten times as much.

3.One link gets a hundred times more clicks than any other. What breaks, and what do you do?

It becomes a hot key: every request lands on the one cache node holding it. Replicate that entry to several cache nodes or hold it briefly in each server’s memory, and let the edge serve it so most clicks never reach you.

4.A link was returned as 301 for a month and is now malicious. Why can't you fully stop it?

Browsers that cached the 301 go straight to the destination without asking your servers again, so disabling the link only stops clients that have not cached it. That is the case for 302.

💡 Learning tip

Close this tab and redraw the final design from memory in five minutes. Then add one box for the analytics exercise. Drawing it cold twice, a day apart, fixes the shape far better than reading it again.

In One Picture#

text
Requirements  shorten · redirect · alias/expiry
Scale         40 writes/s · 4,000 reads/s · 9 TB
Codes         number blocks -> scramble -> base62
Reads         edge -> cache -> replicas in 3 zones
Redirect      302 by default (301 = browser caches)
Abuse         rate limit · check URL · instant takedown
Scale         split read/write · ranges per region

Conclusion#

A URL shortener is a read-path problem wearing a write-path costume. Get the estimate first, and the design follows: simple, collision-free codes on the write side, a cache and replicas on the read side, and a redirect code that keeps you in control of every link. The same shape, a cheap write and a hot read, returns in the next problems of this module.

Which deep dive would you pick first in the room? Tell us in the comments.

🧭 Where to go from here
  • Revisit the maths: back-of-envelope estimation derives every number used here.
  • Go deeper on the read path: caching strategies covers invalidation, the hard part of the cache above.
  • Next in Module 4: design a rate limiter, the box guarding link creation in the final design.

Frequently asked questions

How do you generate short codes for a URL shortener? +
Give each new URL a unique number and encode it in base62 (a-z, A-Z, 0-9). Hand out numbers in blocks to each server so no single counter sits on the write path, and scramble the number before encoding if codes must not be guessable.
Should a URL shortener return a 301 or a 302 redirect? +
Usually a 302. Browsers may cache a 301 without asking again, so later clicks never reach your servers: you lose click counts, expiry and the ability to take a bad link down. Use 301 only when links are permanent and you do not need any of those.
How many characters does a short code need? +
Six base62 characters give about 56.8 billion codes, enough for 6 billion links. Seven give about 3.5 trillion, which leaves room to scramble codes so they cannot be guessed in order.
What database should a URL shortener use? +
Any store with fast lookup by key. The access pattern is one key, one value. A relational database with the code as primary key works at this scale; a key-value store is the natural choice once you partition. The cache, not the database, serves most reads.
What should a short link return after it expires? +
410 Gone, which tells clients the link was removed on purpose and is likely gone for good. Use 404 for codes that never existed. Check expiry on every read and delete expired rows with a background job.
How do you stop a URL shortener from being used for phishing? +
Rate-limit link creation, check each destination against a URL reputation service when the link is created, re-check popular links later, and keep redirects as 302 so a link you take down stops working for everyone.

References

  1. RFC 9110, HTTP Semantics, section 15.4 (redirection status codes)
  2. Redis documentation, INCR
  3. Google Cloud, Web Risk overview (updated October 13, 2026)
  4. RFC 3986, URI Generic Syntax, section 6 (normalization)
  5. Twitter, Snowflake (archived repository, 2010)
  6. Redis documentation, High availability with Redis Sentinel
  7. Cloudflare Workers documentation, Redirect example (updated April 23, 2026)
  8. Wikipedia, Bloom filter
Written by
Sukhveer Kaur
Sukhveer KaurSoftware Developer & AI Engineer

Sukhveer is a software developer specialising in AI systems and backend engineering. She has hands-on experience designing agentic AI applications, working with large language model pipelines, autonomous agent frameworks, and cloud-native services in Java and Python. At InfoWok, she bridges the gap between cutting-edge AI research and practical implementation — helping developers understand and apply emerging technologies through clear, experience-backed writing.

New AI engineering guides, the day they ship

Real Python, production depth. No digest spam.

Comments