Designing a URL Shortener
A URL shortener converts a long URL into a compact identifier that redirects users to the original destination. The product appears simple, but designing it at large scale introduces important system design questions around identifier generation, storage, caching, database partitioning, redirects, availability, and abuse prevention.
It is also a useful system design interview problem because the basic architecture can be established quickly, leaving time to explore the decisions that matter: how short codes are generated without collisions, how billions of mappings are stored, and how a read-heavy redirect path remains fast and highly available.
Table of Contents
- Requirements and Scale Estimation
- API and Data Model
- Short Code Generation
- Designing the Redirect Path
- Database Scaling and Partitioning
- Reliability and Failure Handling
- Production Design Example
- Common Mistakes
- Interview Checklist
- Conclusion
- Key Takeaway
- More Articles to Read
Requirements and Scale Estimation
Before selecting databases or caches, define the important behavior.
Core functional requirements might be:
- create a short URL from a long URL;
- redirect a short URL to its original destination;
- optionally support expiration;
- optionally allow custom aliases;
- collect basic usage statistics.
Useful non-functional requirements include:
- very low redirect latency;
- high availability;
- durable URL mappings;
- horizontal scalability;
- redirects should continue working during partial failures.
The workload is normally read-heavy. URLs are created once but may be redirected many times.
Assume:
New URLs/day: 10 million
Redirects/day: 1 billion
Peak multiplier: 4×
Retention: 5 years
Average URL record: 500 bytes
Average writes:
10,000,000 / 86,400
≈ 116 writes/second
Average redirects:
1,000,000,000 / 86,400
≈ 11,600 reads/second
With a four-times peak:
Peak redirects:
11,600 × 4
≈ 46,400 requests/second
The approximate read-to-write ratio is:
1,000,000,000 : 10,000,000
= 100 : 1
This ratio strongly influences the design. The redirect path deserves aggressive optimization, while URL creation throughput is comparatively modest.
Five years of URL mappings produce:
10M × 365 × 5
≈ 18.25 billion URLs
At approximately 500 bytes per record:
18.25B × 500 bytes
≈ 9.1 TB raw data
Indexes, replicas, backups, and database overhead increase the actual physical requirement.
The purpose of these calculations is not exact infrastructure sizing. They establish that the system contains billions of small records, moderate write traffic, and substantially higher read traffic.
For the estimation methodology behind these calculations, see Estimating Scale and Capacity Planning.
API and Data Model
The external API can remain small.
Creating a short URL:
POST /urls
{
"url": "https://example.com/articles/system-design"
}
Response:
{
"short_url": "https://short.example/aZ91Kd"
}
The redirect endpoint is simply:
GET /aZ91Kd
The server looks up aZ91Kd and returns an HTTP redirect to the original URL.
A minimal data model might contain:
URLMapping
short_code
original_url
created_at
expires_at
user_id
The dominant access pattern is:
short_code --> original_url
That should influence the primary key or index design.
For example:
CREATE TABLE url_mappings (
short_code VARCHAR(10) PRIMARY KEY,
original_url TEXT NOT NULL,
user_id BIGINT,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP
);
There is no requirement to search every URL by arbitrary attributes on the redirect path. Designing indexes around hypothetical queries would increase storage and write cost without improving the main workload.
Short Code Generation
The most recognizable part of the problem is generating compact unique identifiers.
A short code must provide enough combinations for the expected number of URLs.
A common alphabet is Base62:
0-9 = 10 characters
a-z = 26 characters
A-Z = 26 characters
Total = 62 characters
A six-character code provides:
62^6
≈ 56.8 billion combinations
Seven characters provide:
62^7
≈ 3.5 trillion combinations
That does not automatically mean every possible value can be safely allocated. The generation strategy determines collision behavior, predictability, and coordination requirements.
Random Codes
One approach generates a random Base62 string:
import secrets
import string
ALPHABET = string.ascii_letters + string.digits
def generate_short_code(length: int = 7) -> str:
return "".join(
secrets.choice(ALPHABET)
for _ in range(length)
)
The service then attempts to insert the generated code:
Generate code
|
v
Attempt INSERT
|
+--> success --> return short URL
|
+--> collision --> generate another code
The database must enforce uniqueness. A separate read-before-write check is not sufficient because concurrent requests can both observe that a code is free before attempting to insert it.
Advantages:
- simple generation;
- no central sequence required;
- codes are difficult to enumerate sequentially;
- generation can happen independently across application instances.
Disadvantages:
- collisions are possible;
- collision probability grows as the namespace fills;
- creation logic needs retry handling.
Base62-Encoded IDs
Another strategy generates a unique numeric ID and encodes it using Base62.
For example:
Database ID:
12583901
|
| Base62 encoding
v
Short code:
qM8Zx
A simple encoder can look like:
ALPHABET = (
"0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
def encode_base62(value: int) -> str:
if value == 0:
return ALPHABET[0]
encoded = []
while value:
value, remainder = divmod(value, 62)
encoded.append(ALPHABET[remainder])
return "".join(reversed(encoded))
Advantages:
- no collisions when input IDs are unique;
- compact representation;
- easy to implement.
Disadvantages:
- the system needs a globally unique ID strategy;
- simple sequential IDs make URLs easier to enumerate;
- a centralized ID generator can become a dependency or bottleneck if poorly designed.
| Property | Random Base62 | Base62 Unique ID |
|---|---|---|
| Collision possibility | Yes | No, if IDs are unique |
| Central coordination | Usually unnecessary | Depends on ID generation |
| Predictability | Low | Potentially high |
| Retry on creation | Possible | Normally unnecessary |
| Implementation | Simple | Simple if unique IDs already exist |
The interview does not require declaring one approach universally superior. The important part is recognizing the trade-off between collision handling and unique-ID coordination.
Designing the Redirect Path
The redirect path is the most performance-sensitive operation.
A basic implementation is:
Client
|
v
Load Balancer
|
v
Redirect Service
|
v
Database
|
v
Original URL
At tens of thousands of redirects per second, repeatedly reading popular mappings from the database is unnecessary.
A cache can serve frequently accessed URLs:
Client
|
v
Load Balancer
|
v
Redirect Service
|
v
Cache
/ \
HIT MISS
| |
| v
| Database
| |
| populate cache
| |
+------------+
|
v
HTTP Redirect
Pseudocode for the lookup path:
def resolve_url(short_code: str) -> str | None:
cached = cache.get(short_code)
if cached is not None:
return cached
mapping = repository.find_by_short_code(short_code)
if mapping is None:
return None
cache.set(
short_code,
mapping.original_url,
ttl=3600,
)
return mapping.original_url
A read-heavy workload with highly skewed popularity can produce a strong cache hit rate because a relatively small subset of URLs may account for a large portion of redirects.
However, caching introduces additional questions:
- What TTL should mappings use?
- What happens when a URL expires?
- Should deleted mappings be invalidated immediately?
- What happens when the cache fails?
- How are extremely popular hot keys handled?
If URL mappings are immutable after creation, caching becomes considerably easier because invalidation is rare.
Negative caching can also reduce repeated database queries for nonexistent short codes:
GET /invalid123
|
v
Cache MISS
|
v
Database MISS
|
v
Cache "not found" briefly
The negative TTL should generally remain short so that temporary or newly created states are not incorrectly hidden for long periods.
301 vs 302 Redirects
The service also needs to choose the HTTP redirect behavior.
| Redirect | Behavior | Implication |
|---|---|---|
| 301 / permanent | Destination is considered permanent | Browsers and intermediaries may cache aggressively, reducing future service traffic |
| 302 / temporary | Client continues consulting the shortener | More redirect traffic reaches the service, which can improve centralized tracking and control |
If detailed click analytics and destination changes are important, temporary redirects may provide more control. If reducing redirect-service traffic is more important and destinations are permanent, cacheable permanent redirects can be useful.
This is another example where product requirements determine the infrastructure behavior.
Database Scaling and Partitioning
The estimated dataset contains billions of mappings. At sufficient scale, the database may need partitioning.
The main lookup is:
short_code --> original_url
That makes the short code a natural partitioning input.
A simple hash-based strategy can be represented as:
hash(short_code) % number_of_partitions
For example:
aZ91Kd --> hash --> Partition 2
b82LmQ --> hash --> Partition 4
xP09Ka --> hash --> Partition 1
The architecture becomes:
Redirect Service
|
v
Partition Router
/ | \
/ | \
v v v
Shard 1 Shard 2 Shard 3
Hash distribution helps spread unrelated short codes across storage nodes.
Partition count requires care. A simple modulo function tied directly to the number of physical shards makes adding a shard expensive because many keys move.
Production systems can use logical partitions, consistent hashing, or another indirection layer so physical topology can change without remapping most of the dataset.
Database replicas can provide additional read capacity and availability:
Primary Shard
/ \
v v
Replica 1 Replica 2
But replication introduces consistency considerations. A newly created short URL written to the primary may not immediately appear on an asynchronous replica.
One solution is to route recent writes to the primary or populate the cache as part of successful creation:
Create mapping
|
v
Write database
|
v
Populate cache
|
v
Return short URL
The first redirect can then succeed through the cache even while replicas are catching up.
Reliability and Failure Handling
A URL shortener is often expected to remain available because short URLs may appear in emails, documents, advertisements, QR codes, and external websites long after creation.
The redirect path should therefore avoid unnecessary dependencies.
A useful production design separates URL creation from URL resolution:
+-------------------+
| Creation Service |
+-------------------+
|
v
Database
Users
|
v
+-------------------+
| Redirect Service |
+-------------------+
|
+------> Cache
|
+------> Database
If analytics processing fails, redirects should still work. Click events can be published asynchronously:
Redirect request
|
v
Resolve URL
|
+------------------> return redirect
|
+--> Analytics Queue
|
v
Consumers
The critical path remains small:
Client
|
v
Redirect Service
|
v
Cache / Database
|
v
Redirect
Analytics, aggregation, reporting, and other secondary features should not normally block it.
Failure behavior should be explicit:
| Failure | Expected Behavior |
|---|---|
| Application instance fails | Load balancer routes traffic to healthy instances |
| Cache node fails | Requests fall back to database while cache capacity recovers |
| Database replica fails | Traffic uses another replica or primary according to policy |
| Analytics queue unavailable | Redirect availability is prioritized over analytics completeness |
| One availability zone fails | Remaining zones retain sufficient serving capacity |
The cache failure case deserves special attention. If a cache serving 90% of reads disappears, database traffic can increase approximately tenfold almost immediately.
Before cache failure:
100,000 requests/sec
90,000 --> cache hits
10,000 --> database
After cache failure:
100,000 --> database
A database sized only for normal cache-miss traffic may collapse during this event. Cache architecture therefore needs to consider degraded-state database capacity, request throttling, cache recovery, and hot-key protection.
Production Design Example
A practical large-scale architecture can separate creation, redirects, storage, caching, and analytics:
Clients
|
v
+--------------+
| Load Balancer|
+--------------+
/ \
/ \
v v
+----------------+ +----------------+
| URL Creation | | Redirect |
| Service | | Service |
+----------------+ +----------------+
| / \
| v v
| Cache URL Store
| |
v |
URL Store |
|
+----------+
|
v
Analytics Queue
|
v
Analytics Workers
|
v
Analytics Store
The creation flow is:
1. Validate original URL
2. Generate short code
3. Attempt durable insert
4. Retry if random-code collision occurs
5. Optionally populate cache
6. Return short URL
A simplified implementation might look like:
def create_short_url(original_url: str) -> str:
validate_url(original_url)
for _ in range(5):
code = generate_short_code()
try:
repository.insert(
short_code=code,
original_url=original_url,
)
cache.set(
code,
original_url,
ttl=3600,
)
return code
except DuplicateShortCode:
continue
raise RuntimeError("Unable to allocate short code")
The database unique constraint is the authority for collision detection.
The redirect flow is:
1. Receive short code
2. Check cache
3. On miss, query URL store
4. Reject missing or expired mapping
5. Populate cache
6. Publish analytics event asynchronously
7. Return HTTP redirect
A simplified resolver:
def redirect(short_code: str):
original_url = cache.get(short_code)
if original_url is None:
mapping = repository.find(short_code)
if mapping is None or mapping.is_expired():
return not_found()
original_url = mapping.original_url
cache.set(
short_code,
original_url,
ttl=3600,
)
analytics.publish({
"short_code": short_code,
})
return temporary_redirect(original_url)
In production, analytics publication should not be allowed to make the core redirect unavailable. Depending on the required analytics guarantees, event publication can use bounded timeouts, buffering, or another mechanism that keeps secondary work away from the latency-sensitive path.
The most useful metrics include:
- redirect requests per second;
- redirect latency percentiles;
- cache hit ratio;
- database read and write latency;
- short-code collision rate;
- not-found rate;
- expired URL rate;
- database partition traffic distribution;
- hot-key frequency;
- analytics queue depth;
- error rate by availability zone.
Cache hit ratio is particularly important because it changes database capacity requirements dramatically.
Suppose peak traffic reaches 50,000 redirects per second:
95% cache hit rate:
Cache:
47,500 reads/sec
Database:
2,500 reads/sec
If the hit rate unexpectedly drops to 70%:
Database:
15,000 reads/sec
The database load increases sixfold without any increase in user traffic.
Capacity planning should therefore model changes in cache effectiveness, not only changes in request volume.
Common Mistakes
| Mistake | Why It Causes Problems | Better Approach |
|---|---|---|
| Starting with identifier generation | The design ignores requirements, traffic ratios, and storage scale. | Establish requirements and workload before selecting the code-generation strategy. |
| Assuming random IDs never collide | Random generation provides probability, not guaranteed uniqueness. | Enforce uniqueness in durable storage and retry collisions. |
| Checking uniqueness before inserting | Concurrent creators can race between the check and insertion. | Use an atomic unique constraint as the final authority. |
| Using a central ID generator without discussing availability | The generator can become a bottleneck or single point of failure. | Explain how IDs remain unique and available across multiple instances. |
| Ignoring the read/write ratio | The design may optimize URL creation while redirects dominate traffic. | Focus scaling decisions on the read-heavy redirect path. |
| Putting analytics synchronously in the redirect path | Secondary processing increases latency and creates another availability dependency. | Process analytics asynchronously when exact synchronous recording is unnecessary. |
| Assuming cache failure is harmless | Database traffic can increase by an order of magnitude instantly. | Capacity-plan cache failure and control fallback traffic. |
| Partitioning without considering rebalancing | Changing the physical shard count can require moving a large fraction of keys. | Use logical partitions or another strategy that supports topology changes. |
| Ignoring expired and deleted URLs | Caches can continue serving mappings that should no longer resolve. | Define expiration, invalidation, and cache TTL behavior explicitly. |
| Adding unnecessary services | The design becomes complicated before actual bottlenecks are identified. | Keep the redirect path small and add components for concrete requirements. |
Interview Checklist
- Clarify features: define creation, redirects, expiration, aliases, and analytics scope.
- Estimate reads and writes separately: establish the read-heavy nature of the workload.
- Estimate stored mappings: calculate the order of magnitude over the required retention period.
- Define the main access pattern: optimize
short_code → original_url. - Select an identifier strategy: compare random codes with encoded unique IDs.
- Enforce uniqueness atomically: avoid race-prone read-before-write collision checks.
- Optimize redirects: cache popular mappings and keep the critical request path small.
- Choose redirect semantics: explain permanent versus temporary redirects according to product needs.
- Plan database partitioning: use a key that distributes mappings predictably at scale.
- Consider replica consistency: account for redirects immediately following URL creation.
- Keep analytics asynchronous: prevent secondary features from reducing redirect availability.
- Plan cache failure: understand how cache misses affect database capacity.
- Handle expiration: coordinate durable records and cached mappings.
- Protect the service: consider malicious destinations, abusive creation, enumeration, and rate limits.
- Monitor the critical path: track redirect latency, cache effectiveness, storage health, and partition imbalance.
Conclusion
A URL shortener demonstrates several fundamental system design principles in a compact problem. The workload is usually highly read-heavy, the primary data access pattern is simple, and caching can remove most reads from durable storage. At larger scale, identifier generation, database partitioning, cache behavior, and failure handling become the important design decisions.
The architecture should remain centered on the product's most important operation: resolving a short code into a destination with low latency and high availability. Analytics and other secondary capabilities should not unnecessarily increase the complexity or failure surface of that path.
Key Takeaway
Design the URL shortener around the redirect path, not around the short-code algorithm. Estimate the workload first, choose a code-generation strategy with explicit uniqueness semantics, store mappings according to their primary lookup key, cache popular redirects, keep secondary work asynchronous, and plan for the database load that appears when the cache or another layer fails.
Comments (0)