How to Design a URL Shortener: A Practical System Design Guide

URL shortener system design architecture by Jalish Mahmud showing CDN, load balancer, API gateway, Redis cache, database, and message queue.

Learn how to design a scalable URL shortener like Bitly by exploring requirements, API design, database structure, caching, short-code generation, redirection, scalability, and system architecture.

Introduction

After learning the fundamentals of system design, the best next step is to apply those concepts to a real-world problem.

One of the most common beginner-friendly system design examples is a URL shortener.

A URL shortener converts a long URL like:

https://example.com/products/category/electronics/laptops/macbook-pro-16

into something much shorter:

https://short.ly/a8X3kP

When the user opens the short URL, the system redirects them to the original URL.

At first glance, this looks very simple.

But when millions of users start creating and opening short links, several interesting system design problems appear:

  • How do we generate unique short codes?

  • How do we store billions of URLs?

  • How do we redirect users quickly?

  • How do we prevent duplicate codes?

  • How do we scale the system?

  • Where should we use caching?

  • How do we handle expired URLs?

  • How do we track clicks?

In this article, we will design a URL shortener step by step.


1. Understand the Requirements

Before choosing technologies, we should first understand what the system needs to do.

Functional Requirements

The system should allow users to:

  1. Submit a long URL.

  2. Receive a short URL.

  3. Open the short URL.

  4. Get redirected to the original URL.

  5. Optionally set an expiration date.

  6. Optionally track click statistics.

For example:

Input:
https://example.com/blog/system-design-fundamentals

Output:
https://short.ly/A7kP2x

When someone opens:

https://short.ly/A7kP2x

they should be redirected to:

https://example.com/blog/system-design-fundamentals

2. Non-Functional Requirements

The system should also be:

  • Highly available

  • Fast

  • Scalable

  • Reliable

  • Secure

  • Fault tolerant

Redirection should be extremely fast because users should not notice any delay.

A reasonable target could be:

Redirect response time < 100 ms

The application should also handle much more read traffic than write traffic.

Creating a short URL happens once.

Opening that short URL may happen thousands or millions of times.

So this is mainly a read-heavy system.


3. High-Level Architecture

A simple architecture could look like this:

Users
  ↓
Load Balancer
  ↓
API Gateway
  ↓
URL Service
  ├── Cache
  └── Database

The system has two main flows:

Create Short URL

and:

Redirect Short URL

Let's look at both.


4. Creating a Short URL

Suppose a user wants to shorten this URL:

https://example.com/articles/system-design

The client sends:

POST /api/urls

with:

{
  "url": "https://example.com/articles/system-design"
}

The backend generates a unique code such as:

B7xK92

Then it stores:

B7xK92 → https://example.com/articles/system-design

in the database.

The API returns:

{
  "shortUrl": "https://short.ly/B7xK92"
}

5. Redirecting a Short URL

Now a user opens:

https://short.ly/B7xK92

The request reaches our backend.

The system extracts:

B7xK92

and looks for its corresponding original URL.

B7xK92
    ↓
Database
    ↓
https://example.com/articles/system-design

Then the server returns an HTTP redirect.

For example:

HTTP/1.1 302 Found
Location: https://example.com/articles/system-design

The browser automatically opens the original URL.


6. API Design

We can keep the API relatively simple.

Create Short URL

POST /api/urls

Request:

{
  "url": "https://example.com/products/123"
}

Response:

{
  "shortCode": "X7pQa9",
  "shortUrl": "https://short.ly/X7pQa9"
}

Redirect

GET /X7pQa9

Response:

302 Found

and the browser is redirected to the original URL.


Optional Analytics Endpoint

GET /api/urls/X7pQa9/stats

Response:

{
  "clicks": 12540,
  "createdAt": "2026-09-27",
  "lastVisitedAt": "2026-09-27T14:15:00Z"
}

7. Database Design

A basic table could look like this:

urls
--------------------------------
id
short_code
original_url
created_at
expires_at
user_id

Example:

id: 1024

short_code:
X7pQa9

original_url:
https://example.com/products/123

created_at:
2026-09-27

expires_at:
2027-09-27

A SQL version might look like:

CREATE TABLE urls (
  id BIGSERIAL PRIMARY KEY,
  short_code VARCHAR(10) UNIQUE NOT NULL,
  original_url TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  expires_at TIMESTAMP NULL
);

The most important field is:

short_code

because redirects will search by that value.

Therefore, it should be indexed.

CREATE UNIQUE INDEX idx_short_code
ON urls(short_code);

8. How Do We Generate the Short Code?

This is one of the most interesting parts of the design.

We need something like:

a8X3kP

The code should be:

  • Short

  • Unique

  • Easy to store

  • Easy to generate

A common approach is Base62 encoding.

Base62 uses:

0-9
a-z
A-Z

That gives us:

62 possible characters

If we use 6 characters:

62^6

we can generate more than:

56 billion

possible combinations.

That is enough for many systems.


9. Base62 Example

Suppose a database record gets this numeric ID:

125346

We can convert it to Base62.

Example output:

W7K2

Then our URL becomes:

https://short.ly/W7K2

Conceptually:

Database ID
    ↓
Base62 Encoding
    ↓
Short Code

This approach has an advantage:

Database ID is already unique.

Therefore, the encoded value will also be unique.


10. Random Code Generation

Another approach is to generate random characters.

Example:

const characters =
  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

Then generate something like:

Qa8K2x

However, random generation introduces a possible problem:

collision

Two requests might generate the same code.

Therefore, the application must check whether the generated value already exists.

Pseudo-code:

let code;

do {
  code = generateRandomCode();
} while (await database.exists(code));

For smaller systems, this can work well.

For very large systems, we may use more advanced strategies.


11. Database Choice

Both relational and NoSQL databases can work.

Possible relational databases:

PostgreSQL
MySQL

Possible NoSQL databases:

DynamoDB
Cassandra
MongoDB

For a smaller system, PostgreSQL is often more than enough.

For a very large distributed system with billions of records and extremely high traffic, a distributed key-value database may become attractive.

The core access pattern is simple:

short_code → original_url

That is a perfect key-value lookup.


12. Why Caching Is Important

Imagine a popular short URL is opened:

5 million times

If every request queries the database, it creates unnecessary load.

Instead, we can cache frequently accessed URLs.

For example:

Redis

The request flow becomes:

User
 ↓
URL Service
 ↓
Redis Cache
 ↓
Database

The service first checks Redis.

Pseudo-code:

const cachedUrl = await redis.get(shortCode);

if (cachedUrl) {
  return redirect(cachedUrl);
}

If the value is not cached:

const url = await database.findByShortCode(shortCode);

await redis.set(shortCode, url.originalUrl);

return redirect(url.originalUrl);

13. Cache Hit

Suppose:

X7pQa9

already exists in Redis.

The flow is:

Request
 ↓
Redis
 ↓
URL Found
 ↓
Redirect

This is very fast.


14. Cache Miss

If Redis does not contain the value:

Request
 ↓
Redis
 ↓
Not Found
 ↓
Database
 ↓
Store in Redis
 ↓
Redirect

This pattern is commonly called:

Cache-Aside Pattern

It reduces database traffic significantly.


15. Load Balancer

Initially, we may have only one backend server.

User
 ↓
Server

As traffic increases, we can add more servers.

              ┌── Server 1
Users → Load Balancer
              ├── Server 2
              └── Server 3

The load balancer distributes requests.

This gives us:

  • Better scalability

  • Better availability

  • Fault tolerance

If one server fails, requests can be sent to another healthy server.


16. Horizontal Scaling

Instead of making one machine continuously bigger, we can add more machines.

This is called:

Horizontal Scaling

Example:

1 Server
↓
3 Servers
↓
10 Servers
↓
100 Servers

Because URL redirection servers can be mostly stateless, horizontal scaling is relatively easy.


17. Stateless Application Servers

A stateless server does not depend on local memory to handle future requests.

For example, avoid storing important URL data directly inside:

Server 1 Memory

because the next request may go to:

Server 3

Instead, shared data should live in:

Database
Cache
Object Storage

This allows any application server to handle any request.


18. Handling Popular URLs

Imagine a celebrity publishes:

https://short.ly/live2026

and millions of users click it within minutes.

This creates a hot key.

The same short code receives huge traffic.

Caching helps significantly.

Instead of millions of database queries:

Database
Database
Database
Database
Database

most requests are handled from:

Redis

For extremely large systems, we may also use:

  • Distributed caching

  • Multiple Redis replicas

  • CDN or edge caching

  • Request throttling


19. Using a CDN

For global users, requests may come from many countries.

A user in Bangladesh may be far from a server in the United States.

A CDN can bring redirect handling closer to the user.

The architecture could evolve into:

User
 ↓
CDN / Edge
 ↓
Load Balancer
 ↓
URL Service
 ↓
Cache
 ↓
Database

Popular CDN platforms include:

Cloudflare
AWS CloudFront
Fastly
Akamai

This can reduce latency.


20. URL Expiration

Some short URLs may only be valid for a limited period.

For example:

expires_at = 2026-12-31

When a request arrives, the system checks:

Current Time < Expiration Time

If expired, it may return:

410 Gone

or redirect to an expiration page.

Pseudo-code:

if (url.expiresAt && url.expiresAt < new Date()) {
  return response.status(410).send("Link expired");
}

21. Custom Short URLs

Some users may want:

https://short.ly/my-product

instead of:

https://short.ly/A8kL2p

This requires checking whether:

my-product

already exists.

The system must enforce uniqueness.

Example:

UNIQUE(short_code)

Reserved keywords may also need protection.

For example:

admin
api
login
signup
dashboard

These should usually not be allowed as custom aliases.


22. Analytics

A URL shortener may also track:

  • Number of clicks

  • Country

  • Device

  • Browser

  • Referrer

  • Time of visit

However, analytics should not necessarily slow down redirection.

A bad design would be:

Redirect Request
 ↓
Save Analytics
 ↓
Update Counters
 ↓
Process Device Info
 ↓
Redirect

The user waits for everything.

Instead, we can use asynchronous processing.


23. Message Queue for Analytics

A better approach:

User opens URL
 ↓
Redirect Service
 ↓
Return Redirect Immediately
 ↓
Publish Click Event
 ↓
Message Queue
 ↓
Analytics Worker

Example event:

{
  "shortCode": "X7pQa9",
  "timestamp": "2026-09-27T14:30:00Z",
  "country": "BD",
  "device": "mobile"
}

The analytics worker processes the event later.

Possible queue technologies include:

Kafka
RabbitMQ
AWS SQS
Google Pub/Sub

This keeps the redirect path fast.


24. Updated Architecture

Now our architecture looks more realistic.

Users
  ↓
CDN
  ↓
Load Balancer
  ↓
API Gateway
  ↓
URL Service
  ├── Redis Cache
  ├── Database
  └── Message Queue
           ↓
     Analytics Worker

Each component has a specific responsibility.


25. Read Flow

When someone opens:

https://short.ly/X7pQa9

the flow might be:

User
 ↓
CDN
 ↓
Load Balancer
 ↓
URL Service
 ↓
Redis

If Redis has the URL:

Redirect immediately

Otherwise:

Redis
 ↓
Database
 ↓
Store in Redis
 ↓
Redirect

At the same time:

Click Event
 ↓
Message Queue
 ↓
Analytics Service

26. Write Flow

Creating a URL looks like:

User
 ↓
API Gateway
 ↓
URL Service
 ↓
Generate Short Code
 ↓
Database
 ↓
Return Short URL

Example:

Input:
https://example.com/blog

Generated Code:
Ab9Kp2

Result:
https://short.ly/Ab9Kp2

27. Handling Database Failure

What happens if the database becomes unavailable?

Existing popular URLs may still exist in Redis.

So some redirects can continue working.

However, new URLs may not be created.

A production system may use:

  • Database replicas

  • Automated failover

  • Backups

  • Multi-zone deployment

  • Health checks

For example:

Primary Database
      ↓
Replica 1
Replica 2

If the primary fails, another database may take over.


28. Database Replication

Read replicas can help reduce database load.

Example:

            ┌── Read Replica 1
Primary DB ─┼── Read Replica 2
            └── Read Replica 3

Writes go to:

Primary Database

Reads may go to replicas.

However, replication can introduce:

Replication Lag

This means a newly created URL may not instantly appear on every replica.

System designers need to consider this consistency tradeoff.


29. Partitioning and Sharding

If the database grows to billions of URLs, one database server may eventually become insufficient.

We can divide data across multiple databases.

This is called:

Sharding

For example:

Shard 1
A-F

Shard 2
G-M

Shard 3
N-S

Shard 4
T-Z

Another approach is hashing:

hash(shortCode) % numberOfShards

For example:

hash("X7pQa9") % 4

determines which database stores the URL.

Sharding improves scalability but increases system complexity.

It should usually be introduced only when necessary.


30. Security Considerations

Even a URL shortener needs security.

Users may try to shorten malicious URLs.

Possible protections include:

  • HTTPS

  • Rate limiting

  • Spam detection

  • Malware scanning

  • URL validation

  • Abuse reporting

  • Domain blocklists

We should validate that submitted URLs use acceptable protocols.

For example:

https://
http://

and reject dangerous or unsupported schemes.


31. Preventing Abuse

Without rate limiting, someone could generate millions of URLs automatically.

We may apply limits such as:

Anonymous User:
20 URLs per hour

Authenticated User:
500 URLs per day

An API Gateway or Redis-based rate limiter can help enforce these limits.


32. Monitoring

A production URL shortener should monitor:

  • Requests per second

  • Redirect latency

  • Error rate

  • Database latency

  • Cache hit rate

  • Cache misses

  • Queue length

  • CPU usage

  • Memory usage

  • Number of new URLs

For example:

Cache Hit Rate: 94%
Average Redirect Time: 32 ms
Error Rate: 0.03%

These metrics help identify bottlenecks.


33. Cache Hit Ratio

One particularly important metric is:

Cache Hit Ratio

For example:

100,000 requests

95,000 served from Redis
5,000 served from database

The cache hit ratio is:

95%

A high hit ratio significantly reduces database traffic.


34. Avoid Overengineering

A beginner might see this architecture and immediately think they need:

Kafka
Kubernetes
Redis Cluster
Database Sharding
Multiple Regions
20 Microservices

for their first URL shortener.

Usually, they do not.

A simple first version could be:

Next.js / React
 ↓
Node.js API
 ↓
PostgreSQL

Then add Redis when traffic increases.

Application
 ├── Redis
 └── PostgreSQL

Then add multiple application servers.

Load Balancer
 ↓
Application Servers
 ↓
Redis
 ↓
Database

Architecture should evolve as the problem grows.


35. Simple Version vs Large-Scale Version

Small Application

Client
 ↓
Backend
 ↓
PostgreSQL

This may support a surprisingly large number of users.

Growing Application

Client
 ↓
Load Balancer
 ↓
Backend Servers
 ↓
Redis
 ↓
PostgreSQL

Large-Scale Application

Users
 ↓
CDN
 ↓
Load Balancer
 ↓
API Gateway
 ↓
URL Services
 ├── Distributed Cache
 ├── Sharded Database
 └── Message Queue
         ↓
   Analytics Workers

The right architecture depends on actual traffic and business requirements.


36. Important System Design Lessons

This URL shortener teaches several useful concepts.

1. Start with requirements

Do not choose technologies first.

Understand the problem first.

2. Identify read and write patterns

A URL shortener is heavily read-oriented.

3. Use caching for frequently accessed data

Popular redirects are perfect candidates for caching.

4. Keep application servers stateless

This makes horizontal scaling easier.

5. Move non-critical work asynchronously

Analytics should not slow down redirects.

6. Design for failure

Servers, caches, databases, and networks can fail.

7. Scale gradually

Do not introduce complexity before it becomes necessary.


Final Architecture

Our final conceptual architecture is:

                    Users
                      ↓
                     CDN
                      ↓
                Load Balancer
                      ↓
                 API Gateway
                      ↓
                  URL Service
                /      |       \
               /       |        \
          Redis     Database   Message Queue
                                ↓
                         Analytics Worker

The main redirect flow is:

User
 ↓
Load Balancer
 ↓
URL Service
 ↓
Redis
 ↓
Database if needed
 ↓
Redirect

The analytics flow is:

Redirect Request
 ↓
Publish Click Event
 ↓
Message Queue
 ↓
Analytics Worker

Conclusion

A URL shortener is a simple product that introduces many important system design concepts.

At its core, the system only needs to store:

Short Code → Original URL

But once traffic grows, we start thinking about:

  • Load balancing

  • Caching

  • Database indexing

  • Short-code generation

  • Horizontal scaling

  • Message queues

  • Analytics

  • Replication

  • Sharding

  • Availability

  • Security

  • Monitoring

The most important lesson is that system design should grow with the application.

A good engineer does not start by building the most complex architecture.

A good engineer starts with the simplest reliable solution and introduces additional components when real requirements justify them.