How to Design a URL Shortener: A Practical System Design Guide
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-16into something much shorter:
https://short.ly/a8X3kPWhen 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:
Submit a long URL.
Receive a short URL.
Open the short URL.
Get redirected to the original URL.
Optionally set an expiration date.
Optionally track click statistics.
For example:
Input:
https://example.com/blog/system-design-fundamentals
Output:
https://short.ly/A7kP2xWhen someone opens:
https://short.ly/A7kP2xthey should be redirected to:
https://example.com/blog/system-design-fundamentals2. 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 msThe 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
└── DatabaseThe system has two main flows:
Create Short URLand:
Redirect Short URLLet's look at both.
4. Creating a Short URL
Suppose a user wants to shorten this URL:
https://example.com/articles/system-designThe client sends:
POST /api/urlswith:
{
"url": "https://example.com/articles/system-design"
}The backend generates a unique code such as:
B7xK92Then it stores:
B7xK92 → https://example.com/articles/system-designin the database.
The API returns:
{
"shortUrl": "https://short.ly/B7xK92"
}5. Redirecting a Short URL
Now a user opens:
https://short.ly/B7xK92The request reaches our backend.
The system extracts:
B7xK92and looks for its corresponding original URL.
B7xK92
↓
Database
↓
https://example.com/articles/system-designThen the server returns an HTTP redirect.
For example:
HTTP/1.1 302 Found
Location: https://example.com/articles/system-designThe browser automatically opens the original URL.
6. API Design
We can keep the API relatively simple.
Create Short URL
POST /api/urlsRequest:
{
"url": "https://example.com/products/123"
}Response:
{
"shortCode": "X7pQa9",
"shortUrl": "https://short.ly/X7pQa9"
}Redirect
GET /X7pQa9Response:
302 Foundand the browser is redirected to the original URL.
Optional Analytics Endpoint
GET /api/urls/X7pQa9/statsResponse:
{
"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_idExample:
id: 1024
short_code:
X7pQa9
original_url:
https://example.com/products/123
created_at:
2026-09-27
expires_at:
2027-09-27A 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_codebecause 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:
a8X3kPThe code should be:
Short
Unique
Easy to store
Easy to generate
A common approach is Base62 encoding.
Base62 uses:
0-9
a-z
A-ZThat gives us:
62 possible charactersIf we use 6 characters:
62^6we can generate more than:
56 billionpossible combinations.
That is enough for many systems.
9. Base62 Example
Suppose a database record gets this numeric ID:
125346We can convert it to Base62.
Example output:
W7K2Then our URL becomes:
https://short.ly/W7K2Conceptually:
Database ID
↓
Base62 Encoding
↓
Short CodeThis 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:
Qa8K2xHowever, random generation introduces a possible problem:
collisionTwo 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
MySQLPossible NoSQL databases:
DynamoDB
Cassandra
MongoDBFor 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_urlThat is a perfect key-value lookup.
12. Why Caching Is Important
Imagine a popular short URL is opened:
5 million timesIf every request queries the database, it creates unnecessary load.
Instead, we can cache frequently accessed URLs.
For example:
RedisThe request flow becomes:
User
↓
URL Service
↓
Redis Cache
↓
DatabaseThe 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:
X7pQa9already exists in Redis.
The flow is:
Request
↓
Redis
↓
URL Found
↓
RedirectThis is very fast.
14. Cache Miss
If Redis does not contain the value:
Request
↓
Redis
↓
Not Found
↓
Database
↓
Store in Redis
↓
RedirectThis pattern is commonly called:
Cache-Aside PatternIt reduces database traffic significantly.
15. Load Balancer
Initially, we may have only one backend server.
User
↓
ServerAs traffic increases, we can add more servers.
┌── Server 1
Users → Load Balancer
├── Server 2
└── Server 3The 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 ScalingExample:
1 Server
↓
3 Servers
↓
10 Servers
↓
100 ServersBecause 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 Memorybecause the next request may go to:
Server 3Instead, shared data should live in:
Database
Cache
Object StorageThis allows any application server to handle any request.
18. Handling Popular URLs
Imagine a celebrity publishes:
https://short.ly/live2026and 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
Databasemost requests are handled from:
RedisFor 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
↓
DatabasePopular CDN platforms include:
Cloudflare
AWS CloudFront
Fastly
AkamaiThis can reduce latency.
20. URL Expiration
Some short URLs may only be valid for a limited period.
For example:
expires_at = 2026-12-31When a request arrives, the system checks:
Current Time < Expiration TimeIf expired, it may return:
410 Goneor 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-productinstead of:
https://short.ly/A8kL2pThis requires checking whether:
my-productalready exists.
The system must enforce uniqueness.
Example:
UNIQUE(short_code)Reserved keywords may also need protection.
For example:
admin
api
login
signup
dashboardThese 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
↓
RedirectThe 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 WorkerExample 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/SubThis 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 WorkerEach component has a specific responsibility.
25. Read Flow
When someone opens:
https://short.ly/X7pQa9the flow might be:
User
↓
CDN
↓
Load Balancer
↓
URL Service
↓
RedisIf Redis has the URL:
Redirect immediatelyOtherwise:
Redis
↓
Database
↓
Store in Redis
↓
RedirectAt the same time:
Click Event
↓
Message Queue
↓
Analytics Service26. Write Flow
Creating a URL looks like:
User
↓
API Gateway
↓
URL Service
↓
Generate Short Code
↓
Database
↓
Return Short URLExample:
Input:
https://example.com/blog
Generated Code:
Ab9Kp2
Result:
https://short.ly/Ab9Kp227. 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 2If 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 3Writes go to:
Primary DatabaseReads may go to replicas.
However, replication can introduce:
Replication LagThis 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:
ShardingFor example:
Shard 1
A-F
Shard 2
G-M
Shard 3
N-S
Shard 4
T-ZAnother approach is hashing:
hash(shortCode) % numberOfShardsFor example:
hash("X7pQa9") % 4determines 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 dayAn 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 RatioFor example:
100,000 requests
95,000 served from Redis
5,000 served from databaseThe 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 Microservicesfor their first URL shortener.
Usually, they do not.
A simple first version could be:
Next.js / React
↓
Node.js API
↓
PostgreSQLThen add Redis when traffic increases.
Application
├── Redis
└── PostgreSQLThen add multiple application servers.
Load Balancer
↓
Application Servers
↓
Redis
↓
DatabaseArchitecture should evolve as the problem grows.
35. Simple Version vs Large-Scale Version
Small Application
Client
↓
Backend
↓
PostgreSQLThis may support a surprisingly large number of users.
Growing Application
Client
↓
Load Balancer
↓
Backend Servers
↓
Redis
↓
PostgreSQLLarge-Scale Application
Users
↓
CDN
↓
Load Balancer
↓
API Gateway
↓
URL Services
├── Distributed Cache
├── Sharded Database
└── Message Queue
↓
Analytics WorkersThe 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 WorkerThe main redirect flow is:
User
↓
Load Balancer
↓
URL Service
↓
Redis
↓
Database if needed
↓
RedirectThe analytics flow is:
Redirect Request
↓
Publish Click Event
↓
Message Queue
↓
Analytics WorkerConclusion
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 URLBut 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.