URL Shortener System Design: A Walkthrough With Diagram
By Aniruddha · September 25, 2026 · 8 min read
"Design a URL shortener" is the classic system design interview question. It looks small (take a long URL, return a short one) but it touches most of the building blocks you get asked about: ID generation, databases, replication, caching, HTTP semantics, and async processing.
I build a diagram tool, so I drew the whole design and animated it. Here is the 51 second version. The rest of this post goes through each part in more depth, including the things a short video has to leave out.
1. Requirements
Pin these down first. In an interview, they decide every trade-off that follows.
Functional
- Given a long URL, return a unique short URL.
- Opening the short URL redirects to the original.
- Count clicks per link.
Non-functional
- Fast redirects. A redirect sits in front of every visit, so it has to add as little latency as possible.
- Highly available. If the service is down, every link anyone ever shared is broken.
- Read heavy. A link is created once and clicked many times. A 100 to 1 read to write ratio is the usual assumption.
- Codes should not be guessable, so nobody can walk the keyspace and list every link.
A quick estimate
Assume 100 million new links a month. That is about 40 writes per second on average, and at 100 to 1, about 4,000 redirects per second. If each record (code, long URL, timestamps, owner) is about 500 bytes and you keep links for five years, that is 6 billion rows, around 3 TB. Big, but ordinary for a single sharded relational or key-value store.
2. The full diagram
Here is the whole system. Writes go down the left side, reads down the right, and click analytics hang off the read path.

Get the editable diagram
An .excalidraw file. Open it on excalidraw.com, or in a CanvasKeep diagram with Open from the menu.
3. Generating short codes
The code is the part after the domain, like aZ3kP9x in sho.rt/aZ3kP9x. Base62 (a-z, A-Z, 0-9) keeps it URL safe. Seven characters give 62^7, about 3.5 trillion codes, far more than the 6 billion from the estimate above.
There are three common ways to produce them:
- Hash the long URL. Take MD5 or SHA-256 of the URL, Base62 encode it, keep the first 7 characters. The same URL always gets the same code, which is handy, but truncating the hash means collisions, so every write needs a check and a retry.
- Encode a counter. Give each link the next number from a distributed ID generator and Base62 encode it. No collisions, but consecutive codes are predictable, which breaks the "not guessable" requirement.
- Pre-generate keys (what the diagram uses). A Key Generation Service (KGS) creates random unused codes ahead of time and stores them. The Write API just asks for the next one. No collision checks at write time, and codes look random.
The KGS needs two safeguards. First, a code must never be handed out twice, so the KGS marks keys as used the moment it gives them out. It can load a batch into memory for speed and accept that a crash wastes that batch, which is harmless with trillions of codes available. Second, it is a single point of failure, so run a standby replica.
4. The write path
POST /shorten
{ "url": "https://www.example-store.com/blog/2026/09/how-we-scaled..." }- The load balancer routes the request to the Write API.
- The Write API takes an unused key from the KGS.
- It saves
code → long URLto the primary database. - The primary replicates the row to the read replicas.
- It responds with
201 Createdand the short URL.
HTTP/1.1 201 Created
{ "short_url": "sho.rt/aZ3kP9x" }Splitting the Write API from the Read API lets each scale on its own. Writes are rare. Reads need many more instances.
5. The read path and caching
When someone opens sho.rt/aZ3kP9x, the browser sends GET /aZ3kP9x, and the load balancer routes it to the Read API. Traffic is uneven: a rule of thumb is that 20% of links get 80% of clicks. So the Read API checks an in-memory cache such as Redis first, and only goes to the database on a miss. This is the cache-aside pattern:
url = cache.get("aZ3kP9x") // miss
url = replica.get("aZ3kP9x")
cache.set("aZ3kP9x", url)With LRU eviction, the cache naturally keeps the popular links and drops the rest. Misses go to read replicas rather than the primary, so redirects never compete with writes.
6. The redirect: 301 or 302?
The Read API answers with a redirect status and the original URL in the Location header:
HTTP/1.1 302 Found Location: https://www.example-store.com/blog/2026/09/how-we-scaled...
Which status code you pick matters:
- 301 Moved Permanently. Browsers cache it. The first click reaches you, but repeat clicks from that browser go straight to the destination. Less load, but those repeat clicks go uncounted.
- 302 Found. Temporary, and not cached by default, so every click passes through your servers and can be counted.
Since this design counts clicks, it uses 302. If you did not need analytics, 301 would be the cheaper choice. Either is defensible in an interview as long as you explain the trade-off.
7. Click analytics, off the hot path
Writing an analytics row during the redirect would slow down every click. Instead, the Read API publishes a small event to a queue (Kafka, SQS, or similar) and returns the redirect immediately:
{ "code": "aZ3kP9x", "ts": 1790243130, "ref": "x.com" }Consumers read from the queue and aggregate events into an analytics database: clicks per link per day, referrers, countries. If the analytics side falls behind, redirects are unaffected.
8. What the 60 second version leaves out
- Replication lag. A link opened seconds after creation might not be on the replicas yet. Fix it by falling back to the primary when a replica misses, or by writing new links into the cache at creation time.
- Unknown codes. Return
404, and cache the "not found" result briefly, so someone hammering random codes cannot push all that traffic onto the database. - Abuse. Rate limit
POST /shortenper user or IP, and check submitted URLs against malware and phishing lists. Shorteners are a favorite tool for hiding bad links. - Custom aliases. Let users request a code like
sho.rt/launch, check that it is free, and store it in the same table. - Expiration. Store an optional expiry time, treat expired links as not found, and clean them up with a background job.
- Sharding. At billions of rows, partition the URL table by a hash of the code, so lookups by code hit exactly one shard.
Interview checklist
- Clarify requirements and the read to write ratio first.
- Estimate traffic and storage. Size the code length from it.
- Compare hashing, counters, and pre-generated keys. Pick one and say why.
- Separate the write and read paths. Replicate for reads.
- Cache hot links with cache-aside and LRU eviction.
- Choose 301 or 302 and explain the analytics trade-off.
- Keep analytics async so it never slows a redirect.
- Cover edge cases: replication lag, 404s, abuse, expiry.
FAQ
How long should a short code be?
Seven Base62 characters (a-z, A-Z, 0-9) give 62^7, about 3.5 trillion combinations. That covers billions of links with plenty of room left, which is why 6 to 8 characters is the usual choice.
Should a URL shortener use a 301 or 302 redirect?
Use 302 if you want click analytics. Browsers cache a 301, so repeat clicks from the same browser never reach your servers and go uncounted. A 302 is not cached by default, so every click passes through you. 301 saves load when you do not need stats.
How do you generate unique short codes without collisions?
A key generation service pre-generates random unused codes and hands them out, so there is nothing to check at write time. Alternatives are hashing the long URL (needs collision handling) or Base62-encoding a unique counter (codes become guessable).
Why is caching so important for a URL shortener?
Reads far outnumber writes, often by 100 to 1, and a small share of links gets most of the clicks. Keeping popular codes in an in-memory cache like Redis serves most redirects without touching the database.
Sources and further reading
- System Design Primer: Design Pastebin.com (or Bit.ly), for the read and write API split, replicas, and caching.
- Grokking the System Design Interview: Designing a URL Shortening Service, for the key generation service approach.
The diagram was drawn in CanvasKeep, which keeps Excalidraw diagrams in workspaces and folders with version history.