A URL shortener is a service that converts long URLs into compact, easy-to-share aliases. When users access the short URL, they are redirected to the original destination.
Loading simulation...
An url shortener makes links easier to share on social media, track for analytics, and remember.
Popular Examples: tinyurl.com, bitly.com
This problem is a common choice in system design interviews and serves as a great starting point to practice designing scalable, high-traffic systems.
In this chapter, we will explore the high-level design of a URL shortener.
Lets start by clarifying the requirements.
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: "What is the expected scale? How many new URLs per day and how many redirects?"
Interviewer: "Let's aim for 10 million new URLs per day and a 100:1 read-to-write ratio."
Candidate: "What characters are allowed in the shortened URL?"
Interviewer: "Shortened URL can be a combination of numbers (0-9) and characters (a-z, A-Z)."
Candidate: "Should users be able to specify custom aliases for their URLs?""
Interviewer: "Custom aliases are a nice-to-have feature if time permits."
Candidate: "Do short URL need to expire after a certain time?"
Interviewer: "Yes, there should be a default expiration date, with the option for users to override it."
Candidate: "Do we need analytics like click counts?"
Interviewer: "Basic click counts would be good, but advanced analytics is not required."
After gathering the details, we can summarize the key system requirements.
To understand the scale of our system, let’s make some reasonable assumptions.
10,000,000 / (24 * 3600) ≈ 115 QPS (steady state)1,000,000,000 / (24 * 3600) ≈ 11,500 QPS (steady state)Each URL mapping stores:
Total per record: ~300 bytes
10M/day * 365 days = 3.65 billion URLs/year3.65B * 300 bytes = ~1.1 TB/yearUsing Base62 encoding (a-z, A-Z, 0-9):
With 3.65 billion URLs per year, a 7-character code provides ample headroom for decades of growth.
The URL shortener service needs a minimal but powerful set of APIs to create, manage, and resolve shortened links. Below are the core endpoints.
POST /shortenThis endpoint takes a long URL (and optional parameters) and generates a corresponding short URL.
/my-link). If provided, the system validates uniqueness.409 Conflict: If a custom alias already exists.400 Bad Request: If the URL is invalid.401 Unauthorized: If the user is not authenticated.GET /{short_code}This endpoint takes the short URL key and redirects the user to the corresponding long URL.
302 Found: Default. The browser contacts the service on every click, which preserves click tracking and lets us update or expire links.301 Moved Permanently: Optional, for links that never change. It is faster on repeat visits, but the browser caches the destination and stops contacting the service, so click tracking is lost.410 Gone.404 Not Found.GET /analytics/{short_code}Retrieves click statistics for a short URL.
At a high level, our system must satisfy two core requirements:
Since the system has a 100:1 read-to-write ratio (1 billion reads vs. 10 million writes daily), we should design the read path (redirects) to be extremely optimized. We will separate the write service from the read service so each can scale independently.
Instead of presenting the full architecture at once, we'll build it incrementally by addressing one requirement at a time. This approach mirrors how you would explain the design in an interview.
When a user submits a long URL, we need to generate a unique short code, store the mapping, and return the short URL.
Four components work together to serve both write and read traffic. The diagram maps out how they connect, with the load balancer routing URL creation requests to the write path and redirect requests to the read path:
Clients are end-users or applications interacting with the system through web browsers, mobile apps, or third-party integrations. They call POST /shorten to generate short URLs and GET /{short_code} to resolve and access the original URLs.
The Load Balancer sits in front of all application servers and distributes incoming traffic across them. If a server fails, it reroutes traffic to healthy instances, which keeps the system available and fault tolerant. It can also handle SSL termination and basic request filtering such as rate limiting.
This service handles all write operations. It validates incoming URLs, generates a unique short code, handles any custom alias the user provides, and stores metadata such as the expiry date, creation time, and user ID. Since write traffic is relatively low compared to reads, this service doesn’t need to scale aggressively.
The Redirection Service handles the overwhelming majority of traffic, typically billions of GET requests per day, so it must be fast, stateless, and horizontally scalable.
For a given short code, it looks up the original long URL, checks that the link is still valid (not expired or deleted), and issues an HTTP 301 (permanent) or 302 (temporary) redirect based on the system’s policy. It also caches frequently accessed mappings to minimize database hits.
The database stores the mapping between short codes and long URLs, along with relevant metadata. It must support high read throughput to sustain redirection traffic and durable writes so that links are never lost.
The sequence below traces a single shorten request from the client through the load balancer, the write service, and finally into the database:
POST /api/v1/shorten request with the long URL.When a user clicks a short URL, we need to look up the original URL and redirect them.
This service handles all read operations, and since reads dominate (1 billion per day), it must be heavily optimized. For each request it looks up the long URL for the given short code, checks whether the link has expired, and returns an HTTP redirect response.
To achieve sub-50ms latency at 35,000 QPS, we need a distributed cache. It stores frequently accessed URL mappings, which reduces database load for popular links and provides sub-millisecond lookup times.
Because redirects dominate at 100:1, the read path checks Redis first and only falls back to the database on a miss. The sequence below shows both the cache-hit and cache-miss branches, ending in an HTTP 302 redirect to the original URL:
GET /abc123.To choose the right database for our needs, let's consider some factors that can affect our choice:
Given these points, a NoSQL database like DynamoDB or Cassandra is a better option due to their ability to efficiently handle billions of simple key-value lookups and provide high scalability and availability.
Even though our system is simple, we should carefully design the schema to support both redirection and user management. At a minimum, we need two core tables.
Stores the relationship between a short code and its corresponding long URL.
| Field | Type | Description |
|---|---|---|
short_code | String (PK) | Unique identifier, partition key |
long_url | String | Original destination URL |
user_id | String | Creator's user ID (optional) |
created_at | Timestamp | When the link was created |
expires_at | Timestamp | When the link expires |
click_count | Integer | Number of redirects |
is_custom | Boolean | Whether this is a custom alias |
short_code for fast lookups.user_id to list a user's links.Stores user-related information for personalization, analytics, and management.
| Field | Type | Description |
|---|---|---|
user_id | String (PK) | Unique user identifier |
email | String | User's email address |
created_at | Timestamp | Account creation date |
api_key | String | For programmatic access |
Now that we have the high-level architecture and database schema in place, let’s dive deeper into some critical design choices.
The short code generation strategy is one of the most critical aspects of a URL shortener. A good algorithm should produce codes that are unique (no collisions between different URLs), compact (small enough to keep the short link truly "short"), efficient (generated quickly without bottlenecks), and scalable (working across distributed servers).
Let's explore three primary approaches, moving from simple to highly scalable.
This is one of the simplest and most popular techniques for generating short URLs.
The idea is to use a cryptographic hash function to create a unique fingerprint of the long URL, then encode it into a compact, URL-safe string.
Before hashing, the URL must be standardized to avoid generating multiple short links for what is essentially the same destination.
Example: The following URLs all point to the same page but would hash differently if not normalized:
Canonicalization Steps:
:80 for HTTP, :443 for HTTPS).After canonicalization, these all become: https://example.com/
Apply a cryptographic hash function like SHA-256, SHA-1, or MD5 to the canonicalized URL.
For example:
This gives us a fixed-length fingerprint (128 bits for MD5, 256 bits for SHA-256).
We don’t need the full hash, it’s too long for a short link.
Instead, we take the first few bytes (e.g., 48 bits = 6 bytes) and encode them using Base62.
Base62 encoding uses a 62-character set (A-Z, a-z, 0-9) that is URL-friendly. It's preferred over Base64, which includes + and /, characters that have special meaning in URLs and would require escaping.
A 48-bit number, when Base62 encoded, produces a compact code of approximately 8 characters, since 62^8 < 2^48 < 62^9.
Even a 7-character code spans 62^7 ≈ 3.5 trillion combinations, more than enough for most systems.
https://example.com/courses/system-design06d68bdbae12cb1837095f97771be94c06d68bdbae1206d68bdbae12 (hexadecimal) → 7,518,539,197,970 (decimal)28MoyG9aThe specific choice of 6 bytes (48 bits) is important because it produces a decimal number that converts to a Base62 string of approximately 8 characters.
This method is deterministic. The same long URL will always produce the same short code. That makes duplicates easy to detect, and it lets multiple servers generate codes independently without coordination.
However, hash collisions are possible because we truncate the hash.
Two different URLs could (rarely) produce the same short code:
→ Might both hash to → 28MoyG9a
Even though cryptographic hash collisions are rare, truncation reduces the available bit space, making collisions statistically inevitable at scale.
When a newly generated short code already exists in the database (detected via a conditional write that fails if the key is already present, or a UNIQUE constraint violation in a relational store), we need a fallback strategy.
Add a random salt or nonce to the original URL and hash again: hash = sha256(url + salt)
Repeat until a unique short code is found.
Attach a small counter or suffix to the colliding short code:
Both methods eliminate duplicates but come with trade-offs.
Trade-off: Both resolution strategies negate the stateless benefit by requiring at least one database lookup, adding latency and complexity to the write path.
This is one of the simplest and most reliable ways to generate short URLs.
Instead of hashing, it relies on a global, monotonically increasing counter to ensure every short code is unique by design.
The entire system revolves around a centralized counter service that generates the next available integer ID in sequence and never repeats it.
When an application server receives a request to shorten a URL, it contacts a Counter Service typically implemented with a fast in-memory data store like Redis, etcd, or Zookeeper.
Redis provides the INCR command, which is atomic by nature.
Each time this command is called, Redis locks the key, increments its value by one, returns the new value, and releases the lock. This atomicity ensures that even under heavy concurrency, no two servers ever get the same number.
The application server receives the unique integer ID (e.g., 123456789) from the counter service. It then applies Base62 encoding to this number to create the final, compact short code.
1,000 -> Base62 g81,000,000 -> Base62 4c9B1,000,000,000 -> Base62 15ftgGEven after generating a billion short URLs, the code length stays under 6-7 characters, which is extremely efficient.
Putting the steps together, the sequence below shows a client submitting a long URL, the application server calling Redis INCR to get a unique integer, encoding it to Base62, and persisting the mapping before returning the finished short URL:
INCR) per short URL. Sub-millisecond latency in Redis or etcd.To scale and secure this approach, modern URL shorteners extend it in two key ways.
Instead of one global counter, deploy multiple counters, each responsible for a subset of IDs. For example, 1024 counters distributed across regions or shards.
Each 64-bit ID combines a shard ID in the top 10 bits (up to 1024 shards) with a local counter value in the remaining 54 bits.
Formula: global_id = (shard_id << 54) | local_counter
This gives each shard up to 2⁵⁴ (≈18 quadrillion) IDs, far more than the system will ever need, so wraparound is not a concern.
This removes the single bottleneck and enables horizontal scaling, while still guaranteeing global uniqueness as long as shard IDs are unique.
Trade-off: Slightly more complexity in managing shards and ensuring IDs don’t overlap.
To prevent sequential IDs from being easily decoded shuffle bits using a simple Feistel cipher or XOR mask.
The transformation is reversible (so you can still decode IDs if needed). This produces short codes that appear random even though they are derived from sequential IDs.
This adds privacy without sacrificing performance.
This is the industry-standard solution for generating unique, non-deterministic IDs at a massive scale. It's a hybrid approach that avoids the need for a centralized counter.
Each ID is a 64-bit integer made up of multiple components that together guarantee uniqueness even across thousands of machines.
This represents the number of milliseconds since a custom epoch (a chosen start date, e.g., Jan 1, 2020). Using a custom epoch instead of the Unix epoch extends the lifetime of the system.
41 bits can store 2^41 milliseconds, approximately 69.7 years of continuous operation.
So if your epoch starts in 2025, you’ll have ID space until roughly 2094.
Each worker (application server or service instance) gets a unique identifier at startup.
2^10 = 1,024 unique workers per cluster.Worker ID Assignment: When a worker starts up, it connects to a coordination service like ZooKeeper or etcd. It registers itself and requests a unique worker ID, which it holds for its lifetime or for a set "lease" period. This startup coordination is vital to prevent two workers from accidentally using the same ID.
This is a local, in-memory counter that tracks how many IDs the worker has generated within the same millisecond.
2^12 = 4,096 IDs per millisecond per worker.If a worker hits 4,096 requests within one millisecond, it must wait for the clock to tick to the next millisecond before continuing.
The redirection service is the most performance-critical component of a URL shortener.
Every time a user clicks a short link, this service must:
Even tiny delays (a few milliseconds) can degrade user experience at scale, especially when serving billions of redirects per day.
Before optimizing performance, we must decide how to perform the redirect.
The choice between 301, 302, and 307 affects both speed and control.
A 301 redirect tells browsers to permanently caches the redirect. Future clicks on the same short link will go directly to the long URL without contacting your service again.
Pros: Fastest for the end-user after the first visit. Reduces load on your service significantly.
Cons: You lose all control and analytics. You can no longer update the destination URL or count subsequent clicks, as the browser bypasses you completely.
Best for: Links that should never change (e.g., permanent marketing URLs).
A 302 Found or 307 Temporary Redirect tells the browser that the destination might change in the future. The browser contacts your service on every click to resolve the short code.
Pros: Accurate click tracking and analytics. You can update or expire short links anytime.
Cons: Slightly higher latency (each request hits your service). Adds read load to your infrastructure.
Use 302 by default. It provides flexibility, analytics, and updatability, critical for any modern URL shortener.
To achieve low latency globally, we design a "waterfall" lookup path. The system tries to find the long URL in the fastest, closest cache available. It only proceeds to the next, slower layer on a cache miss.
If a 301 redirect was previously issued, the browser already knows the final URL. The request never reaches your servers, the user is instantly redirected.
For global services, a Content Delivery Network (CDN) is essential. CDNs like Cloudflare, Akamai, or Fastly maintain edge servers near users worldwide.
Cache Hit: The CDN instantly serves the cached 302 response with the long URL (≈10-50 ms latency).
Cache Miss: The CDN forwards the request to your origin server.
At your application layer, the first lookup happens in a distributed in-memory cache, keyed by the short code (e.g., xyz) with the long URL as the value.
Cache Hit: The application server immediately returns the long URL. The CDN then caches this response for future users (cache-aside pattern).
Cache Miss: If not found in Redis, the application proceeds to the final layer, the database.
The database holds the authoritative mapping of short codes to long URLs. Since lookups are simple key-value reads, NoSQL stores like DynamoDB, Cassandra, or ScyllaDB are ideal.
Query Example:
Once retrieved:
302 response to the CDN, which caches it.Custom aliases allow users to create human-readable short codes instead of system-generated ones.
For example:
This feature is especially valuable for marketing campaigns, social media sharing, and brand consistency, where memorability and aesthetics matter.
However, supporting custom aliases introduces challenges around validation, uniqueness, and race conditions that must be carefully handled to maintain system integrity.
The API for link creation should accept an optional custom_alias parameter along with the long URL.
If the custom_alias field is not provided, the service falls back to auto-generating a short code using the ID generation algorithm.
Before creating a custom alias, the system must validate it against strict rules to prevent conflicts, misuse, or routing errors.
^[a-zA-Z0-9_-]+$ is typically used to enforce this./api, /admin, /login), common terms (/help, /contact), and any potentially offensive words. This prevents users from hijacking critical application paths.All checks should occur before any database operation to minimize load on backend systems.
This is the most critical aspect of custom alias creation.
Two users could try to claim the same custom alias (summer-sale) at the exact same moment.
summer-sale exists. (It doesn't).summer-sale exists. (It still doesn't).Without proper safeguards, both requests might initially think the alias is free leading to duplicate entries or inconsistent state.
Instead of a separate "check, then write" in application code, push the uniqueness guarantee down to the storage layer with a single atomic conditional write that only succeeds if the key does not already exist.
The custom alias occupies the same key space as generated short codes (it is stored as the short_code), so a redirect resolves both kinds of code the same way.
The exact primitive depends on the store:
PutItem with a condition expression attribute_not_exists(short_code).INSERT ... IF NOT EXISTS, a lightweight transaction.PRIMARY KEY or UNIQUE constraint on the short code, with the insert rejected on violation.Example (DynamoDB conditional write):
If the conditional write fails because the key is taken, the service returns 409 Conflict for a custom alias, or generates a new code and retries for an auto-generated one.
The redirect service is critical. Here's how to ensure 99.99% availability.
Deploy services across multiple geographic regions. A primary region handles all writes, each region keeps read replicas of the database, and latency-based DNS routes users to the nearest region.
For DynamoDB:
For Cassandra:
LOCAL_QUORUM for reads to ensure consistency within a region.If the database is unreachable:
With the core design complete, two common extensions remain: expiring links and counting clicks.
Link expiration is essential for maintaining both security and data hygiene.
It is common in time-sensitive campaigns such as marketing or event promotions, security-critical links like password resets or invitations, and routine system maintenance to prevent indefinite data growth.
Without proper expiration handling, a URL shortener’s database could grow endlessly, degrading performance and increasing storage costs.
There are two primary ways to handle expired links: Active Deletion and Passive Expiration.
In this model, a scheduled worker process periodically scans the database for expired links and deletes them.
Example:
Typically, this runs every hour or day using a background job scheduler such as Cron, Celery, or Airflow.
In this approach, the link is never physically deleted when it expires.
Instead, the application layer checks the expiration timestamp during the redirect lookup.
Logic:
A hybrid strategy combines the precision of passive expiration with the efficiency of occasional cleanup.
Every redirect request performs a real-time expiration check, so users experience instant and accurate expiration behavior.
A low-frequency background job (weekly or monthly) deletes or archives long-expired links. This prevents unbounded growth without adding continuous load to the database.
A crucial design consideration is ensuring all cache layers respect link expiration.
If not handled carefully, caches might continue serving redirects after the link has expired.
Suppose a link expires in 5 minutes (300 seconds), but its CDN or Redis cache entry is set with a TTL of 24 hours. Even after the link expires, cached redirects keep getting served, which violates user expectations and can create security risks.
When writing to any cache, set the cache TTL to the smaller of:
TTL_cache = min(link.remaining_lifetime, default_cache_ttl)
This ensures cache TTLs align with link lifetimes to prevent expired redirects from being served.
A modern URL shortening system is not complete without analytics. The most fundamental metric is click count, the number of times each shortened link is accessed.
This data powers insights such as how many people clicked a link, which campaigns perform best, and where and when traffic originates.
However, counting clicks accurately at scale introduces challenges related to performance, consistency, and real-time aggregation.
A click is registered whenever a user follows a shortened link that results in a successful redirect to the target URL.
To prevent double counting, a few conditions are typically applied: only HTTP 302/301 redirects count, requests that result in expired, invalid, or bot traffic are excluded, and repeated requests from the same user within a short time window may be ignored.
When a user clicks a short link, two things happen simultaneously:
To avoid slowing down the redirect path, these actions are separated into synchronous and asynchronous operations.
This decoupled design ensures the redirect latency remains low while analytics are processed asynchronously.
Each redirect increments a counter directly in the database or cache.
For high-traffic systems, direct increments are inefficient. Instead, accumulate clicks in memory or a fast cache and flush them periodically to the database.
Workflow
Large-scale systems such as Bitly or Google Analytics use event-driven pipelines.
Each click generates an event that flows through systems like:
This enables real-time dashboards and flexible queries across billions of clicks.
20 quizzes