# Design a URL Shortener

Course: [High-Level Design](https://www.devtutorials.ai/learn/system-design/hld) · Section: HLD Case Studies
Source: https://www.devtutorials.ai/learn/system-design/hld/case-studies/url-shortener

> Design QuickBite's link shortener end to end: requirements, traffic and storage estimates, code length, 301 vs 302, base62 codes from ID ranges, caching, and a design sandbox.

QuickBite sends many links every day.

Every order confirmation includes a tracking link. Restaurants share their menu links on social media. These URLs are often long and difficult to share.

Example:

https://quickbite.app/restaurants/spice-garden-koramangala/menu?ref=sms&campaign=diwali-2026

A URL shortener converts that long URL into a shorter link such as `qbite.link/3xK9pQ`.

When someone opens the short link, they are redirected to the original URL.

At first, this sounds simple.

Store a short code, store a long URL and look it up later.

That simplicity is what makes URL shorteners a classic system design problem. The idea is small enough that you can focus on the design process.

**The main idea:** a URL shortener is a read-heavy lookup system. Every code must be unique, and redirects must be fast and reliable.

## What You Will Learn

In this case study, you will:

1. Write the requirements for a URL shortener.
2. Estimate traffic, storage and code length.
3. Design the APIs and choose the correct redirect type.
4. Compare different code generation approaches.
5. Design the high-level architecture.
6. Build and test the design under real traffic.

This lesson builds on Estimation, Load Balancing, Caching, Data Storage, Partitioning and Rate Limiting.

## Step 1: Requirements

Before designing the system, define what it must do.

### What It Must Do (Functional)

- Create short links from long URLs.
- Redirect users from short URLs to long URLs.
- Count link clicks.
- Support link expiration.
- Return link information when requested.
- Handle large numbers of redirects.

### How Well It Must Do It (Non-Functional)

- Redirects should complete within 100 ms at p99.
- The service should remain highly available.
- Every short code must be unique.
- Codes should be difficult to guess.
- The system should scale as traffic grows.
- Redirects should remain reliable during failures.

### Out of Scope

This lesson does not include:

- Custom aliases
- User accounts
- Editing existing links

The focus is URL shortening and redirection.

## Step 2: Estimate

Before designing the architecture, estimate traffic.

Assume QuickBite creates:

- 1 million new links per day
- 100 clicks per link
- Peak traffic is 5 times average traffic

Divide by the 86,400 seconds in a day, then multiply by 5 for the peak:

| Traffic | Average | Peak |
|---|---|---|
| New links | 11.6 a second | 58 a second |
| Redirects | 1,157 a second | 5,787 a second |

What to notice:

- Writes are very small.
- Redirects are much larger.
- Reads are about 100 times more common than writes.
- This is a read-heavy system.
- Caching becomes very important.

### Storage

Suppose:

- Each link requires about 500 bytes.
- Data is stored for 5 years.
- Three copies of the data are kept.

1 million links a day for 5 years is **1.825 billion links**. At 500 bytes each, that is **0.91 TB** for one copy, or **2.74 TB** with 3 copies.

What to notice:

- Storage requirements are reasonable.
- A single database can hold the data.
- Traffic is a bigger challenge than storage.

### How Long Should the Code Be?

Short codes use Base62 characters: `0-9`, `a-z` and `A-Z`. This gives 62 possible characters.

Every new character multiplies the number of possible codes by 62.

The chart shows how much of each code length 1.825 billion links would use up.

What to notice:

- Five characters are not enough.
- Six characters are sufficient.
- Seven characters provide room for future growth.
- Longer codes are harder to guess.

QuickBite chooses **7-character Base62 codes**.

Estimation helps make design decisions. Here it shows that redirects need optimization, storage is manageable and 7-character codes provide enough capacity.

## Step 3: The API

Only two APIs are required.

| API | Purpose |
|---|---|
| `POST /links` | Create a short link |
| `GET /{code}` | Redirect to the original URL |

### Create Link

| Part | Value |
|---|---|
| Request | `POST /links` |
| Input | Long URL, expiry date (optional) |
| Response | `201 Created`, short code `3xK9pQ` |

### Use Link

| Part | Value |
|---|---|
| Request | `GET /3xK9pQ` |
| Response | `302 Found` with `Location: https://...` |
| Code missing or expired | `404 Not Found` |

Link creation should also be rate limited to prevent abuse.

### 301 or 302?

There are two common redirect responses.

**301 Moved Permanently**

- Browser may cache the redirect.
- Future clicks may skip the shortener.

**302 Found**

- Browser asks the shortener every time.
- Every click can be counted.

Because click counting is a requirement, QuickBite uses `302 Found`.

What to notice:

- 301 reduces traffic.
- 302 preserves click tracking.
- Requirements matter more than saving requests.

## Step 4: Generating Short Codes

The most important design decision is how to generate codes.

### Option 1: Hash the URL

Hash the long URL and keep the first few characters.

Advantages:

- Simple
- Same URL gets the same code

Problem:

- Different URLs can produce the same short code.
- Collision checks are required.

As the number of URLs grows, collisions become likely.

The chart shows the chance that at least two links get the same 7-character code.

### Option 2: Counter + Base62

Assign every new link a unique number.

The first links get 1, 2, 3 and so on. Then convert the number into Base62. Step through one conversion:

Here is the same idea in code:

```js runnable title="A counter turns into a short code"
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

function toBase62(n) {
  if (n === 0) return "0";
  let code = "";
  while (n > 0) {
    code = ALPHABET[n % 62] + code;
    n = Math.floor(n / 62);
  }
  return code;
}

for (const n of [1, 61, 62, 125, 1_000_000_000]) {
  console.log(`${String(n).padStart(10)} -> ${toBase62(n)}`);
}
```

Expected output:

```text
         1 -> 1
        61 -> Z
        62 -> 10
       125 -> 21
1000000000 -> 15FTGg
```

Advantages:

- Every code is unique.
- No collision checks.
- Fast writes.

The challenge is the counter.

A single counter becomes a bottleneck.

### ID Ranges

Instead of requesting one number at a time:

- Server A receives IDs 1-1000
- Server B receives IDs 1001-2000

Each server creates codes locally. Step through two servers sharing the work:

Here is the same idea in code:

```js runnable title="Each server takes a block of 1,000 numbers"
let nextBlockStart = 1_000_000_000;
function takeBlock() {
  const block = { next: nextBlockStart, end: nextBlockStart + 999 };
  nextBlockStart += 1000;
  return block;
}

const serverA = takeBlock();
const serverB = takeBlock();
const newId = (server) => server.next++;

console.log(`Server A: ${newId(serverA)}, ${newId(serverA)}`);
console.log(`Server B: ${newId(serverB)}, ${newId(serverB)}`);
console.log(`Trips to the range service for 2,000 links: 2`);
```

Expected output:

```text
Server A: 1000000000, 1000000001
Server B: 1000001000, 1000001001
Trips to the range service for 2,000 links: 2
```

What to notice:

- Very few calls are needed.
- No collisions occur.
- Writes remain fast.

### Option 3: Random Codes

Generate a random 7-character code.

Before saving:

1. Check if the code exists.
2. If it exists, generate another code.
3. Save when a free code is found.

Advantages:

- Simple
- Difficult to guess

Disadvantages:

- Requires a database check.
- Collisions must be handled.

### QuickBite's Choice

QuickBite selects **Counter + ID Ranges + Base62**.

Because:

- Codes are unique by design.
- No collision checks are needed.
- Code generation is fast.
- The system scales easily.

## Step 5: The High-Level Design

There are two different paths.

### Redirect Path

This is the busy path.

Around 5,800 redirects happen every second during peak traffic.

Flow:

1. User opens short URL.
2. Request reaches the load balancer.
3. App server checks the cache.
4. On a cache miss, read from the database.
5. Store the result in cache.
6. Return a 302 redirect.
7. Send click events to a queue.

Why use a queue?

Because counting clicks should never slow down redirects.

Step through one redirect, first as a cache miss and then as a hit:

### Create Path

This path is much smaller.

Around 60 requests per second at peak.

Flow:

1. User creates a short link.
2. Request passes through a rate limiter.
3. App server receives an ID from its local range.
4. Convert ID to Base62.
5. Save the mapping in the database.

### Data Model

A simple table is enough.

| Field | Example |
|---|---|
| code | 3xK9pQ |
| long_url | https://quickbite.app/... |
| created_at | timestamp |
| expires_at | timestamp |

Lookups happen using the code.

The code is the primary key.

If growth becomes very large, partition data by code.

### Cache Size

Most clicks occur on recently created links.

Older links receive very little traffic.

Suppose:

- 10 million links are clicked each day.
- The hottest 20% receive most traffic.

10 million links × 20% × 500 bytes = **about 1.0 GB** of cache for the hot links.

What to notice:

- Hot links fit comfortably in memory.
- Cache greatly reduces database traffic.
- Redirect performance improves significantly.

## Step 6: Build It on the Canvas and Test It

Open the **design sandbox**.

The starting design includes:

- 1 load balancer
- 2 app servers
- 1 database
- No cache
- No queue

The simulator generates:

- Link creation traffic
- Redirect traffic
- Infrastructure failures

| Check | Passes when |
|---|---|
| Peak load | p99 at most 100 ms and under 0.1% errors |
| Lose any one machine | Errors under 1% and p99 under 250 ms |
| Cost | At most $1,500 per month |
| Requirements | 302 redirects and unique code generation |

Run the starting design first.

Then add:

- Cache
- Queue
- ID range service
- Additional servers

Compare the results.

What you will probably find:

- One load balancer creates a single point of failure.
- Redirect traffic overloads the database without caching.
- Database writes for click tracking slow down redirects.
- Caching improves performance dramatically.
- Queues simplify click tracking.
- ID ranges eliminate code-generation bottlenecks.

With only one cache node, the "Lose 1 cache node" check sends most requests to the database at once, and p99 jumps to about 366 ms. Keep a second cache node, or give the database enough copies to cope without the cache.

A design that passes:

- 2 load balancers
- 4 app servers
- 2 cache nodes
- 2 database copies
- ID range service
- Queue

This design costs about **$1,360 per month** and keeps p99 latency around **22 ms**.

## Common Mistakes

| Mistake | Wrong | Correct | Why It Happens |
|---|---|---|---|
| Ignoring estimates | Start designing without numbers | Estimate traffic first | Numbers drive architecture |
| Hash collisions | Assume hashes never collide | Handle collisions or avoid them | Collisions are underestimated |
| Wrong redirect type | Use 301 for analytics | Use 302 | Browser caching hides clicks |
| Single counter | One central counter | ID ranges | Central counters become bottlenecks |
| Sequential codes | Easy-to-guess URLs | Scramble IDs before encoding | Predictable code generation |
| No cache strategy | Depend entirely on cache | Plan for cache failures | Cache outages are often ignored |

## Practice

## What to Remember

- A URL shortener is a read-heavy lookup system.
- Redirect traffic is much larger than create traffic.
- Estimate first before designing.
- Use 7-character Base62 codes.
- Use 302 redirects when clicks must be counted.
- Counter + ID ranges guarantees unique codes.
- Cache should handle most redirect traffic.
- Click tracking should happen through a queue.
- Test designs against peak load, failures and cost.

## Further Reading

These are the sources this lesson was checked against:

- [System Design Primer: Design Pastebin.com (or Bit.ly)](https://github.com/donnemartin/system-design-primer/blob/master/solutions/system_design/pastebin/README.md): a worked short-link design, with estimates and base62 codes.
- [MDN: 301 Moved Permanently](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/301): how browsers treat a permanent redirect.
- [MDN: 302 Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/302): the temporary redirect used here.
- [RFC 9110, HTTP Semantics: Redirection 3xx](https://www.rfc-editor.org/rfc/rfc9110#name-redirection-3xx): the standard that defines these status codes.
- [Wikipedia: Birthday problem](https://en.wikipedia.org/wiki/Birthday_problem): the maths behind hash clashes.

Next: **Rate Limiter Service**, where QuickBite turns rate limiting into a shared service that protects all APIs.
