A URL shortener converts long links into compact aliases that are easier to share, track, and manage.
The core idea is simple: take a long URL, generate a short code, and redirect anyone visiting that short link back to the original destination.
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 we also support custom aliases?"
Interviewer: "Custom aliases are a nice-to-have feature if time permits."
Candidate: "Do links 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 link stores:
Total ≈ 300 bytes per link
Annual storage: 10M links/day * 365 days * 300 bytes ≈ 1.1 TB for the link table per year.
The URL shortener service needs a minimal but powerful set of APIs to create, manage, and resolve shortened links. Below are the two core APIs required for the basic functionality.
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_url_key}This endpoint takes the short URL key and redirects the user to the corresponding long URL.
301 Moved Permanently: Used when the short link is permanent.302 Found: Can be used for temporary redirects (if expiry or temporary policy applies).410 Gone.404 Not Found.At a high level, our system must satisfy two core requirements:
Since the system has a very high read-to-write ratio (billions of reads vs. millions of writes), it is beneficial to separate the URL generation service from the redirection service. This separation allows each service to be scaled independently based on its traffic pattern.
Clients are end-users or applications interacting with the system via web browsers, mobile apps, or third-party integrations (e.g., APIs).
They use:
POST /shorten to generate short URLs.GET /{short_code} to resolve and access the original URLs.The Load Balancer sits in front of all application servers and plays a key role in ensuring high availability and scalability.
Responsibilities:
This service handles all write operations, including creation and management of new short URLs.
Key Responsibilities:
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. It must be ultra-fast, stateless, and horizontally scalable.
Responsibilities:
This component’s design should focuse on low latency, caching efficiency, and high read throughput.
The database stores the mapping between short codes and long URLs, along with relevant metadata.
Must support:
POST /shorten request with the long URL and optional metadata (e.g., custom alias or expiry).GET /abc123).404 Not Found or 410 Gone).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.
Stores user-related information for personalization, analytics, and management.
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 ensure that the generated codes are:
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:
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 (62^7 < 2^48 < 62^8).
This allows ~281 trillion (62⁸) unique short codes, more than enough for most systems.
1b3aabf5266b0f178f52e45f4bb430eb1b3aabf5266b1b3aabf5266b (hexadecimal) → 47770830013755 (decimal)DZFbb43The specific choice of 6 bytes (48 bits) is important because it produces a decimal number that typically converts to a Base62 string of approximately 7 characters.
This method is deterministic. The same long URL will always produce the same short code.
This means:
However, hash collisions are possible because we truncate the hash.
Two different URLs could (rarely) produce the same short code:
→ Might both hash to → DZFbb43
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 UNIQUE constraint violation), 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:
Atomicity ensures that even under extreme concurrency, no two servers can 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.
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 ID combines:
Formula: global_id = (shard_id << 20) | local_counter
This gives each shard up to 2²⁰ (≈1 million) IDs before wraparound.
Benefits:
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 (just 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.
xyz).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 relying on application logic, use the database itself as the source of truth by defining a UNIQUE constraint on the alias column.
Schema Example:
Link expiration is essential for maintaining both security and data hygiene.
It is commonly used in:
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.
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.
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:
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, certain conditions are typically applied:
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.
Pros
Cons
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
1. When a user clicks a link, increment a counter in Redis:
2. A background job periodically aggregates and updates the persistent store:
3. Reset the Redis counter after flushing.
Pros
Cons
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.
Pros
Cons
What is the main purpose of a URL shortener in system design?