URL Shortener System Design: Architecture Diagram and Trade-offs

Design a URL shortener step by step: capacity numbers, short code generation, 301 vs 302 redirects, caching, click analytics and an architecture diagram.

· Founder & developer

Published

URL Shortener System Design: Architecture Diagram and Trade-offs

A URL shortener looks like a weekend project: take a long URL, hand back a short code, redirect when someone clicks it. That is why it shows up in so many system design interviews. The small surface hides real decisions about ID generation, caching, HTTP semantics and analytics, and each one changes the architecture.

This post designs one end to end. It starts with the numbers, walks through how to generate short codes without collisions, settles the 301 versus 302 question, and ends with an architecture diagram you can copy and adapt.

Requirements and numbers

Functional requirements:

  • Create a short link for a long URL, optionally with a custom alias.
  • Redirect a short link to its long URL.
  • Count clicks per link.
  • Expire links when asked to.

Non-functional requirements: redirects must be fast and highly available, short codes must not be guessable in sequence if links can be private, and the system must not become a free redirector for phishing pages.

Back-of-envelope numbers make the shape obvious:

Assumption Value Per second
New links per month 100 million about 40 writes/s
Redirects per month 10 billion about 3,900 reads/s
Read to write ratio 100:1
Record size about 500 bytes
Storage for 5 years 6 billion links about 3 TB

Reads outnumber writes by two orders of magnitude, so the read path gets the cache, the replicas and the attention. The write path is small and can be simple.

The API

Two endpoints carry the product:

POST /links
Content-Type: application/json

{"url": "https://example.com/very/long/path", "alias": "launch", "expiresAt": "2027-01-01T00:00:00Z"}

201 Created
{"code": "launch", "shortUrl": "https://sho.rt/launch"}

GET /launch

302 Found
Location: https://example.com/very/long/path

Keep them on separate services. The redirect service is stateless and read-only, so you can scale it on its own and deploy it rarely.

Generating the short code

The short code is a number written in base62 (a–z, A–Z, 0–9). Seven characters give 62^7, about 3.5 trillion codes; six give about 56.8 billion. At 100 million links a month, seven characters last for thousands of years.

The real question is where the number comes from:

Approach How it works Watch out for
Counter + base62 One global sequence, encoded Single point of contention; codes are sequential and guessable
ID ranges An allocator hands each API instance a block of IDs (say 100,000) Gaps when an instance dies; still sequential within a block
Hash + truncate Hash the URL, keep the first 7 base62 characters Collisions; you need a check-and-retry
Snowflake-style IDs Timestamp + machine ID + sequence in 64 bits Longer codes (11 characters in base62)

Hashing is tempting because the same URL always gets the same code. The catch is the birthday problem: with 3.5 trillion possible codes, the chance of at least one collision passes 50% after only about 2.2 million links. Every insert then needs a uniqueness check and a retry with a salt, which turns a write into a read plus a write.

ID ranges are the pragmatic default. Each API instance reserves a block from the allocator in one small transaction, then hands out codes from memory with no coordination. If you do not want sequential codes, run the ID through a reversible permutation (a block cipher over the ID space, for example) before encoding it. Twitter's Snowflake shows the other route: a 41-bit timestamp, a 10-bit machine ID and a 12-bit sequence give unique 64-bit IDs without any central allocator, at the cost of longer codes.

Custom aliases skip the generator. Store them in the same table and rely on a unique constraint on the code column to reject duplicates.

301 or 302?

RFC 9110 defines the redirect codes. The difference that matters here is caching: a 301 (Moved Permanently) response is heuristically cacheable, so browsers can remember it and skip your server on the next click. A 302 (Found) is temporary and is not cached unless you say so.

  • Use 302 when you need every click counted or want to change the target later.
  • Use 301 when the link never changes and you prefer fewer requests over complete analytics.

307 and 308 are the method-preserving versions of 302 and 301. For plain browser clicks, which are GET requests, the choice between them rarely matters.

Storage and caching

Every redirect is a point lookup by code, so any store that does fast key lookups works: a key-value store such as DynamoDB or Cassandra, or Postgres with the code as the primary key. Shard by the code when one node is no longer enough; a hash of the code spreads load evenly because no code is hotter than another by design. Hot links are the exception, and the cache absorbs them.

Put a Redis cache in front of the store with a cache-aside read: look up the code in Redis, fall back to the database on a miss, then fill the cache with a TTL. Link popularity follows a long tail, so a cache holding a small fraction of links serves most clicks. The caching strategies post covers TTLs, stampedes and invalidation in detail. Read replicas help the fallback path; the database replication post explains the lag you then have to tolerate.

A CDN or edge cache can serve the most popular redirects directly if you send a Cache-Control header, but the edge then answers clicks your servers never see. Either log at the edge or keep redirects uncached.

Click analytics without slowing the redirect

Never write a click count synchronously on the redirect path. The redirect service publishes a small click event (code, timestamp, referrer, country) to a log such as Kafka and returns immediately. A consumer aggregates counts per link per day and writes them to an analytics store. If the analytics side falls behind, redirects keep working; it is the same decoupling described in the event-driven architecture post.

The architecture diagram

A user reaches a CDN and load balancer. GET requests go to a redirect service that reads Redis, then a links store on a miss. POST requests go to a shorten API that takes ID ranges from an allocator, checks URLs with a reputation API and writes links. Clicks go via Kafka to analytics.

13 components · 12 connections

Open in Archboard
Diagram contents
  • User
  • Edge / CDN
  • Load balancer
  • Redirect service
  • Shorten API
  • Redis: code to URL
  • Links store
  • ID allocator
  • URL reputation API
  • Click events
  • Click aggregator
  • Analytics store
  • GET /{code}
  • POST /links
  • 1. lookup
  • 2. on miss
  • insert code and URL
  • ID range
  • check the target
  • click event
  • consume
  • daily counts
A URL shortener with separate read and write paths. Redirects hit a cache first, new links get IDs from pre-allocated ranges, and click events flow to analytics without slowing the redirect.

How to read it:

  • Read path (top). Requests for GET /{code} pass the edge and the load balancer to the redirect service, which checks Redis first and the links store only on a miss.
  • Write path (bottom). POST /links goes to the shorten API. It checks the target with a URL reputation service, takes IDs from a range it reserved from the allocator, encodes them in base62 and writes the link.
  • Analytics (right). Click events are dashed because they are asynchronous. The aggregator turns them into daily counts.
  • The Markdown block keeps the capacity numbers next to the design, so a reviewer can check them.

Abuse, expiry and custom aliases

A public shortener is a phishing tool unless you stop it. Check new targets against a reputation service such as Google Safe Browsing, rate-limit link creation per account and IP, and keep a way to disable a code quickly. Recheck old links periodically, because a clean domain can turn malicious later.

For expiry, store expiresAt with the link and check it on redirect, then delete expired rows with a background job. Return 404 or 410 for expired codes instead of redirecting.

What to put on the diagram

  • Separate the read and write paths. They scale differently, and the diagram should show it.
  • Label the redirect status (301 or 302) on the redirect service. It decides whether analytics is complete.
  • Show where IDs come from. "Generate code" in a box hides the most discussed decision in the design.
  • Mark asynchronous flows as dashed. Readers should see at a glance that analytics cannot slow a redirect.
  • Write the numbers down. A design without writes per second and storage size cannot be reviewed.

The system design diagram example shows the same conventions on a larger system.

Build it in Archboard

  1. Sign in with Google or GitHub and create a diagram.
  2. Add a Person from the C4 Model pack for the user, a CDN and a Cloud LB from the Cloud pack.
  3. Add four Container nodes from the C4 Model pack: the redirect service, the shorten API, the ID allocator and the click aggregator. Put the technology and a one-line description on each.
  4. Add Redis, Kafka and a Data Warehouse from the Data pack, a Database from the C4 Model pack for the links store and an External System for the URL reputation API.
  5. Connect the read path at the top and the write path at the bottom, draw the click events as dashed edges, and add a Markdown block from the Core pack with the capacity numbers.

The example uses 13 diagram nodes, within the Free plan.

FAQ

How long should a short code be? Seven base62 characters give about 3.5 trillion codes, enough for decades at 100 million links a month. Start with seven and let codes grow to eight later; nothing in the design depends on a fixed length.

Should I hash the long URL to get the code? Only if deduplication matters more than simple writes. Truncated hashes collide early (past 2.2 million links for 7 characters there is a better than even chance of at least one collision), so every insert needs a check and a retry.

Is a 301 or a 302 better for a URL shortener? A 302 if you need every click counted or may change the target. A 301 lets browsers cache the redirect, which saves requests but hides repeat clicks from your analytics.

Which database should store the links? Any store with fast lookups by primary key. Postgres is enough for a long time at these numbers; a key-value store becomes attractive when you need to shard writes across many nodes.

How do I stop people from guessing links? Do not expose sequential IDs. Permute the ID before encoding it, or use random codes with a uniqueness check, and never treat a short link as a secret for private content.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.