Designing a Simple URL Shortening Service A TinyURL Approach
0%
Editorial Solution
System requirements
Functional
Non-Functional
Scope Control
Capacity estimation
Traffic
Storage
Bandwidth
Cache Sizing
Why These Numbers Matter
API design
Create Short URL
Redirect
301 vs 302: Why It Matters
Rate Limiting
Database design
Why a Key-Value Store
Schema
Secondary Indices
Why Not PostgreSQL
Why Not Redis Alone
Consistency Model
High-level design
Components
Why This Component Split Works
Request flows
Write Flow (shortenURL)
Read Flow (redirectURL)
Latency by Tier
Detailed component design
Short Code Generation
Collision Handling with Conditional Writes
Caching Strategy
Partitioning
Trade offs/Tech choices
Hash vs Random: A Deeper Look
301 vs 302: Revisited
Database Choices
Pre-Generated Codes vs On-Demand
Failure scenarios/bottlenecks
Cache Stampede
Hot Key Problem
Write Spikes
Data Durability
Database Partition Throttling
Monitoring Essentials
Future improvements
Custom Alias Enhancements
Click Analytics
TTL Auto-Deletion
Abuse Prevention
Link Preview and Metadata
Multi-Region Deployment
Table of Contents
URL shortening is deceptively simple on the surface, accept a long URL, return a short one. But the engineering challenge hides in the operational numbers. URL shorteners are among the most read-heavy systems you will encounter. For every URL someone creates, that link gets clicked roughly 100 times. At scale, this means the system must store billions of entries, handle tens of thousands of redirects per second, and deliver near-instant response times for every single click.
Functional
Shorten URL: Given a long URL, generate a unique short URL (e.g., tinyurl.com/abc123). The short code should be as compact as possible while guaranteeing uniqueness.
Redirect URL: Given a short URL, redirect the user to the original long URL. This is the core operation and must be fast. A slow redirect defeats the entire purpose of the service.
Custom aliases (optional): Allow users to choose a custom short code (e.g., tinyurl.com/my-brand) instead of a system-generated one. This adds namespace management complexity but is a common product requirement.
Link expiration: Support an optional time-to-live (TTL) so links automatically expire after a set duration. Expired links should return a 404 or an error page, not redirect to stale content.
Non-Functional
High availability: The redirect path must be available at all times. A broken short link damages user trust permanently because the user has no way to determine the original destination. URL creation can tolerate brief outages, but redirects cannot.
Low latency: Redirects must complete in under 10ms. Users expect clicking a link to feel instant, and any perceivable delay suggests the link is broken or suspicious. URL creation can tolerate higher latency (a few seconds) since users expect the generation process to take a moment.
Horizontal scalability: The system must scale to billions of stored URLs and tens of thousands of redirects per second. Growth should be handled by adding machines, not by upgrading to a bigger server.
Durability: Once created, a URL mapping must never be lost. Short URLs get embedded in emails, documents, social media posts, QR codes, and printed materials. A lost mapping means every instance of that link across the internet breaks silently with no way to recover.
Scope Control
In a 35-50 minute system design interview, keeping the requirements focused is critical. Start with the core: shorten and redirect. Mention custom aliases and expiration as optional extensions you could discuss if time allows. Avoid expanding into features like user accounts, analytics dashboards, or admin panels during the requirements phase. These are future improvements, not core requirements. A tight scope lets you dive deeper into the architecture instead of spreading thin across features.
The asymmetry between reads and writes is the single most important characteristic of this system. URL creation happens once. URL redirection happens many times. This roughly 100:1 read-to-write ratio shapes every architecture decision you will make.
Traffic
Start with the write side. A mid-size URL shortener creates roughly 200 new short URLs per second. That translates to about 17 million new URLs per day (200 times 86,400 seconds).
Now apply the 100:1 read-to-write ratio. If each shortened URL is clicked an average of 100 times over its lifetime, the redirect path handles approximately 20,000 requests per second. This number is the critical design constraint: it determines your caching strategy, CDN configuration, and database read capacity.
The access pattern is highly skewed. A small fraction of URLs (viral links, popular content) generate the vast majority of redirect traffic. Most URLs are clicked fewer than 10 times. This skew is good news for caching: a relatively small cache can absorb most of the read traffic.
Storage
Each URL mapping entry contains:
- Short code (7 bytes)
- Long URL (approximately 100 bytes average)
- User ID (20 bytes)
- Created timestamp (8 bytes)
- Expiration timestamp (8 bytes)
That totals roughly 143 bytes per entry. Round up to 256 bytes to account for metadata, indexing overhead, and padding.
Daily storage growth: 200 writes/sec times 86,400 seconds times 256 bytes = approximately 4.4 GB per day.
Over 5 years: 4.4 GB times 365 days times 5 years = approximately 8 TB. With growth buffer, plan for 10-15 TB.
Bandwidth
Write bandwidth: 200 requests/sec times 256 bytes = roughly 50 KB/sec. Negligible.
Read bandwidth: 20,000 requests/sec times 256 bytes = roughly 5 MB/sec. Modest by modern standards.
Read bandwidth dominates, but both numbers are manageable for a well-designed system. Neither storage nor bandwidth is the bottleneck.
Cache Sizing
If 20% of URLs account for 80% of traffic (Pareto distribution), we want to cache at least the top 20% of daily active URLs. With 17 million URLs created per day and an average entry size of 256 bytes, caching 20% of the total dataset (about 630 million entries over 5 years) would require roughly 160 GB of Redis memory. In practice, caching just the last 30 days of active URLs (approximately 500 million entries) in a Redis cluster with 200-300 GB of memory provides an 80-90% cache hit rate. This is well within the capacity of a modern Redis cluster deployment.
Why These Numbers Matter
The capacity math reveals an important truth: TinyURL is a throughput problem, not a storage problem. 8 TB fits comfortably in a single modern database cluster. But serving 20,000 reads per second with sub-10ms latency requires caching layers, a CDN, and careful partitioning. The 100:1 ratio tells you exactly where to focus your design energy: on the read path. The cache sizing math confirms that a reasonably sized Redis cluster can absorb most of the read load, keeping DynamoDB requests manageable.
Create Short URL
POST /api/v1/urls
Request body:
- longUrl (required): The destination URL to shorten
- customAlias (optional): A user-chosen short code instead of a system-generated one
- expiresAt (optional): ISO 8601 timestamp for when the link should stop working
Response (201 Created):
- shortCode: The generated or custom short code (e.g., abc123)
- shortUrl: The full short URL (e.g., https://tinyurl.com/abc123)
- longUrl: Echo of the original URL for confirmation
- createdAt: Timestamp of creation
- expiresAt: Expiration timestamp, if set
Authentication: API key in the Authorization header. Every write request must be associated with a registered account for rate limiting and abuse tracking.
Rate limit: 100 URLs per hour per API key for free tier. Higher limits for paid accounts. Returns 429 (Too Many Requests) when exceeded with a Retry-After header indicating when the client can try again.
Validation: The server should validate that the longUrl is a well-formed URL with a supported scheme (http or https). Reject URLs with unsupported schemes (javascript:, data:, ftp:) to prevent abuse. Optionally, check that the URL is reachable with a HEAD request, though this adds latency and is better done asynchronously.
Redirect
GET /shortCode
Returns a 302 (Found) response with the Location header set to the original long URL. The browser follows the redirect automatically. No authentication required. Anyone with the short URL can follow it. This is intentional. Short URLs are shared publicly and must work for everyone who clicks them.
If the short code does not exist or has expired, returns 404 (Not Found) with a JSON body explaining the error. For expired links, include a message indicating the link has expired rather than simply saying "not found." This helps users understand what happened and reduces confusion.
For SEO and social media compatibility, the redirect response should also include standard headers: Cache-Control to control CDN behavior, and optionally X-Robots-Tag to tell search engines whether to index the short URL or the destination URL.
301 vs 302: Why It Matters
This choice has significant architectural implications:
301 (Moved Permanently) tells the browser to cache the redirect. The next time the user clicks the same short link, the browser goes directly to the long URL without contacting the shortener at all. This reduces server load dramatically. The trade-off: you lose visibility into every click. You cannot track analytics, you cannot update the destination URL after creation, and you cannot enforce expiration because the browser never checks back.
302 (Found) tells the browser the redirect is temporary. Every click goes through the shortener. This enables click analytics (you see every redirect), destination updates (change where the link points), and expiration enforcement (the server checks TTL on each request). The trade-off: higher server load because every click hits your infrastructure.
For most URL shorteners, 302 is the right default. The analytics and flexibility outweigh the server load increase, which is handled by CDN caching anyway. The CDN caches the 302 response at edge locations, giving you the load reduction of 301 with the flexibility of 302.
Rate Limiting
Without rate limiting, a single malicious or misconfigured client could create millions of URLs, exhausting the short code space and consuming storage. Rate limiting protects shared resources and ensures fair usage. Implementation: per-API-key counters with sliding window limits, enforced at the API Gateway layer before requests reach the Shortening Service.
Why a Key-Value Store
The core operation of a URL shortener is a simple lookup: given a short code, return the long URL. There are no joins, no multi-table transactions, no aggregation queries, and no complex filtering. This is the exact pattern that key-value stores are optimized for: single-digit-millisecond reads at any scale.
DynamoDB is a strong fit for this workload. It provides fully managed operations with no infrastructure overhead, automatic horizontal scaling via partition-based distribution, single-digit-millisecond reads at any table size, built-in replication across three availability zones, and conditional writes for atomic conflict detection on custom aliases. You provision read and write capacity (or use on-demand mode) and DynamoDB handles the rest.
Schema
- shortCode (partition key): The 7-character Base62 code. This is the primary lookup key for every redirect.
- longUrl: The original destination URL. Average length approximately 100 bytes.
- userId: The account that created this mapping. Links writes to specific users for management and billing.
- createdAt: Creation timestamp. Used for analytics and audit.
- expiresAt: Optional expiration timestamp. DynamoDB TTL uses this field to automatically delete expired items at no read/write cost.
Secondary Indices
Global Secondary Index on longUrl: This index enables duplicate detection. When a user submits a long URL that has already been shortened, the system can check the GSI in O(1) time and return the existing short code instead of creating a new one. This saves storage and avoids the confusion of multiple short codes pointing to the same destination.
Global Secondary Index on userId: This powers the user management dashboard. A user can query all URLs they have created to view click statistics, update destinations, or delete links. Without this index, listing a user's URLs would require a full table scan.
Why Not PostgreSQL
PostgreSQL is a valid choice for a URL shortener handling under 5,000 reads per second with a modest data volume. But as the dataset grows beyond billions of rows and reads exceed 20,000 per second, PostgreSQL requires significant manual work: connection pooling, read replicas, manual sharding, and careful index tuning. DynamoDB handles this automatically through partition-based scaling. That said, if your team has deep PostgreSQL expertise and you are comfortable with the operational overhead, PostgreSQL works.
Why Not Redis Alone
Redis is excellent as a cache but risky as the sole data store. It stores data in memory, which is volatile: a server restart, out-of-memory condition, or failover gap can lose data. Redis persistence modes (RDB snapshots and AOF logs) reduce the risk but introduce write latency and still have small data-loss windows during crashes. For a service where a lost mapping means a permanently broken link with no recovery path, a durable database backing the cache is essential.
Consistency Model
URL shortening can tolerate eventual consistency for a brief window. When a new URL is created, the mapping may not be immediately readable from all DynamoDB replicas or cached in Redis. This window is typically under 1 second for DynamoDB eventually consistent reads, and zero for strongly consistent reads (which cost twice the read capacity). For the redirect path, eventually consistent reads are acceptable: the worst case is a single 404 that resolves on retry. For the write path, range-based allocation guarantees each code is unique at generation time, so the Queue Worker's write to DynamoDB does not require strong consistency for collision detection. Conditional writes remain as a safety net for custom aliases, where two users could request the same alias simultaneously.
The architecture separates into two distinct paths, each optimized for its unique requirements.
Read (Redirect) path: Client click --> CDN --> API Gateway --> Mapping Service --> Redis Cache --> DynamoDB. Optimized for sub-10ms latency with multiple caching layers. The CDN handles the hottest URLs at edge locations. Redis handles the warm set in the data center. DynamoDB is the durable fallback for cache misses.
Write (Shorten) path: Client request --> API Gateway --> Shortening Service --> Message Queue --> Queue Worker --> DynamoDB. Optimized for reliability and database protection. The Shortening Service generates a guaranteed-unique code from its pre-allocated ID range and enqueues the write. The message queue decouples the client response from the durable write, smoothing traffic spikes.
Components
API Gateway: TLS termination, DDoS protection, request routing, and rate limiting for write requests. All external traffic enters through the gateway, providing a single enforcement point for authentication and throttling.
Shortening Service: Generates short codes from a pre-allocated ID range, validates inputs, and enqueues write requests. Each instance owns an exclusive range of IDs, so codes are guaranteed unique at generation time. Stateless and horizontally scalable behind a load balancer.
Mapping Service: Handles redirect lookups. Checks Redis first, falls through to DynamoDB on cache miss. Also stateless. This is the most latency-sensitive component.
Message Queue (SQS or Kafka): Buffers write requests between the Shortening Service and the database. If 2,000 URL creation requests arrive in one second, the queue absorbs the spike and the Queue Worker drains them at a steady 200 per second that the database can comfortably handle.
Queue Worker: Consumes messages from the queue and writes URL mappings to DynamoDB. Since codes are generated from exclusive ID ranges, collisions cannot occur, and every write succeeds on the first attempt. Rate-limited to match database write capacity.
Redis Cache: Stores the warm set of URL mappings (recently and frequently accessed). LRU eviction policy ensures the cache holds the most useful entries. Cluster mode for horizontal scaling across multiple shards.
CDN (CloudFront): Caches redirect responses at edge locations worldwide. For a viral URL, the CDN serves millions of redirects from a nearby edge node without any request reaching the backend. This is the single most impactful component for read performance.
DynamoDB: The durable source of truth for all URL mappings. Handles cache misses from Redis, supports conditional writes for custom alias conflict detection, and provides automatic TTL-based expiration.
Why This Component Split Works
Each component in the architecture has a clear responsibility and can be scaled independently. The CDN scales by adding edge locations (no code changes needed). Redis scales by adding shards to the cluster. The Mapping Service scales by adding stateless instances behind the load balancer. The message queue scales by adding partitions. DynamoDB scales automatically by splitting partitions. No single component needs to handle the full 20,000 reads per second on its own. The CDN absorbs 80-90%, Redis handles most of the rest, and DynamoDB only sees the cold tail of traffic.
The 100:1 read-to-write ratio is not just a capacity planning number. It is the design decision that splits the architecture into two fundamentally different paths. The write path (200/sec) can tolerate message queues, async processing, and higher latency. The read path (20K/sec) demands sub-10ms responses and multiple caching layers. Every component choice flows from this ratio.
In an interview, emphasize that the CDN absorbs the vast majority of redirect traffic before it ever reaches your backend. For a popular URL shortener, 80-90% of redirects hit the CDN edge. Your API servers only handle cache misses. This insight shows you understand where the real load is handled.
Mid-level: describe the high-level components and explain why you separate read and write paths.
Senior: justify the CDN placement, explain the cache hierarchy (CDN vs Redis vs DB), and discuss how the message queue protects the database from write spikes.
Staff: analyze the failure modes of each component, explain how the system degrades gracefully when individual layers fail, and discuss the operational trade-offs of managed services versus self-hosted.
Write Flow (shortenURL)
- Client sends POST /api/v1/urls with the long URL, API key, and optional custom alias or expiration.
- API Gateway validates the API key, checks rate limits, and forwards the request to the Shortening Service.
- Shortening Service takes the next sequential ID from its pre-allocated range and Base62-encodes it to produce the short code. Each Shortening Service instance owns an exclusive range of IDs (e.g., Server A owns 1 to 1,000,000, Server B owns 1,000,001 to 2,000,000), so the generated code is guaranteed unique.
- Shortening Service enqueues a message containing the short code, long URL, user ID, timestamps, and expiration to the message queue.
- Shortening Service returns the short URL to the client immediately (201 Created).
- Queue Worker picks up the message and writes the URL mapping to DynamoDB.
Custom alias path: If the user provided a custom alias, the Shortening Service bypasses the queue and performs a synchronous conditional write to DynamoDB. The conditional write atomically checks that the alias is not already taken. If it succeeds, the alias is confirmed and returned to the client. If it fails (alias already exists), the client receives a 409 Conflict error immediately. Custom aliases cannot use the async queue path because, unlike range-generated codes, two users could request the same alias — returning the alias before confirming availability would risk giving two users the same short URL.
The key insight: the client gets the short URL before the database write completes. This is safe because range-based allocation guarantees each code is unique — no two servers ever generate the same code, so collisions cannot occur. If a user clicks the short URL in the brief window before the write completes, they get a 404 that resolves within seconds.
Why not random generation? With random codes, there is a small but real probability of collision (two requests generating the same code). If the system returns a code to the client before verifying uniqueness, and a collision occurs, the client holds a code that maps to someone else's URL — with no way to learn about the replacement. Range-based allocation eliminates this problem entirely by making each code unique at generation time, before the queue is involved.
Deduplication check: Before generating a new code, the Shortening Service can optionally query the longUrl GSI to check if this URL has already been shortened. If a match is found, the existing short code is returned immediately without enqueuing a new write. This saves storage and provides a consistent user experience: the same long URL always returns the same short code.
Error handling: If the Queue Worker fails to write (e.g., DynamoDB throttling), the message returns to the queue for retry. The durable queue (SQS with at-least-once delivery) ensures no writes are lost. If retries are exhausted, the message moves to a dead-letter queue for manual investigation. The client already received a short URL, so the system must also notify the client that the URL could not be activated — though this is extremely rare with DynamoDB's reliability.
Read Flow (redirectURL)
- User clicks the short URL (GET /abc123).
- The request first hits the CDN. If this short code is cached at the edge location, the CDN returns a 302 redirect immediately. Latency: under 5ms. This handles the majority of traffic for popular URLs.
- CDN miss: the request reaches the API Gateway, which routes it to the Mapping Service.
- Mapping Service checks Redis Cache. If the short code is cached, Redis returns the long URL. Latency: under 10ms. The Mapping Service sends a 302 redirect.
- Redis miss: Mapping Service queries DynamoDB. DynamoDB returns the long URL. Latency: 5-50ms depending on network hops.
- Mapping Service writes the result back into Redis (cache-aside pattern) so subsequent requests for this URL hit the cache.
- Mapping Service returns the 302 redirect to the user.
Latency by Tier
The three-tier architecture creates a dramatic latency difference depending on where the URL is found. A CDN hit resolves in under 5ms because the response comes from a nearby edge location. A Redis hit takes under 10ms because the data is in memory within the data center. A DynamoDB fallback takes 5-50ms depending on network hops and cross-AZ latency. For users, the CDN and Redis paths feel instant, while the DynamoDB path introduces a slight but noticeable delay.
The cache-aside pattern (step 6) ensures that a DynamoDB fallback only happens once per URL per cache TTL period. After the first miss, the URL is loaded into Redis and subsequent requests hit the cache.
This section dives deep into the core engineering challenge of URL shortening: how to generate unique short codes at scale without collisions, and how to cache and partition the data efficiently.
Short Code Generation
A short code uses Base62 encoding: the characters a-z, A-Z, and 0-9, giving 62 possible characters per position. A 7-character code provides 62 to the 7th power, which equals approximately 3.5 trillion possible values. Even at 200 new URLs per second for 10 years (about 63 billion URLs), you would use less than 2% of the available code space.
Approach 1: Hash then truncate. Hash the long URL using MD5 or SHA-256, then take the first 7 characters of the Base62-encoded hash. This is deterministic: the same long URL always produces the same short code, which provides natural deduplication. The problem: truncating a hash dramatically increases collision probability due to the birthday paradox. With 7 characters (3.5 trillion values), the probability of at least one collision reaches 50% at roughly 2 million entries.
Approach 2: Random generation. Generate a random 7-character Base62 string for each URL. This provides excellent distribution across the code space and avoids the truncation problem. The trade-off: two submissions of the same long URL produce different short codes, so you need a separate deduplication mechanism (the longUrl GSI). Collision probability is lower than truncated hashes because the full 7-character space is utilized uniformly.
Approach 3: Range-based counter (chosen approach). A coordination service (ZooKeeper or DynamoDB itself) pre-allocates large ranges of sequential IDs to each application server. Server A gets 1 to 1,000,000. Server B gets 1,000,001 to 2,000,000. Each server generates sequential IDs within its range and Base62-encodes them. Zero collision probability within a range. When a range is exhausted, the server requests a new one. This is the approach used in our write flow because it guarantees code uniqueness at generation time, making the async queue-based write pattern safe. The trade-off: requires a coordination service, and sequential IDs are somewhat predictable (mitigated by shuffling or salting the Base62 encoding).
Collision Handling with Conditional Writes
Even with range-based allocation, the system uses conditional writes as a safety net (especially for custom aliases). DynamoDB conditional writes provide the safest mechanism.
A conditional write (PutItem with the condition that the partition key does not already exist) is atomic: it either succeeds or returns a ConditionalCheckFailedException in a single round-trip. There is no race condition window between checking and inserting because the check and the insert are the same operation.
With range-based allocation, collisions cannot occur because each server owns an exclusive ID range. For range-generated codes, every write succeeds on the first attempt. Custom aliases are the primary use case for conditional writes: two users might request the same alias simultaneously, so the Shortening Service performs a synchronous conditional write (bypassing the async queue) to confirm the alias is available before returning it to the client.
The alternative, a separate read-then-write pattern (first query to check if the code exists, then insert if it does not), has a fatal race condition. Two concurrent requests could both read "code does not exist" and both proceed to insert, with the second silently overwriting the first. Conditional writes eliminate this race entirely.
DynamoDB conditional writes solve the collision problem atomically in a single operation: the write either succeeds (no collision) or fails with ConditionalCheckFailedException (collision detected). There is no race condition window between checking and writing because the check and write are the same operation. This is fundamentally simpler and more reliable than a separate read-then-write approach.
Sequential IDs from range-based allocation are predictable, which means users could enumerate nearby codes by incrementing. Mitigate this by shuffling or salting the Base62 encoding so the output sequence is not guessable. The partition hotspot concern does not apply: DynamoDB hashes the short code partition key internally, distributing sequential codes across partitions evenly.
Caching Strategy
CDN (hot tier): Caches 302 redirect responses at edge locations. Best for viral and popular URLs that receive thousands of clicks per second. TTL-based expiration (e.g., 5 minutes) ensures updates eventually propagate. Limited storage, but the power-law access pattern means a small cache handles most traffic.
Redis (warm tier): In-datacenter cache using cluster mode with multiple shards. Stores millions of URL mappings in memory with LRU eviction. The cache-aside pattern means URLs are only loaded into Redis when someone actually clicks them, ensuring cache space is used for URLs people are actively visiting.
Why cache-aside over write-through: most newly created URLs are never accessed or accessed rarely. Write-through caching would fill Redis with millions of cold entries that nobody clicks, wasting memory. Cache-aside only populates the cache with URLs that generate actual traffic, maximizing the cache hit rate for a given memory budget.
Cache invalidation: When a URL mapping is updated (e.g., destination changed) or deleted, the system must invalidate the corresponding cache entries in both Redis and CDN. For Redis, delete the key directly. For CDN, use cache invalidation APIs (e.g., CloudFront invalidation) or rely on the short CDN TTL (5 minutes) for eventual consistency. In practice, the CDN TTL approach is simpler and the brief staleness window is acceptable for most use cases.
Partitioning
Both Redis and DynamoDB partition by the short code hash. This alignment means a single redirect lookup hits exactly one Redis shard and (on cache miss) exactly one DynamoDB partition. No fan-out reads across multiple shards.
Short codes distribute evenly across partitions because DynamoDB and Redis hash the code internally. Whether the code is generated from a range-based counter or randomly, the partition-level hash function distributes keys uniformly. Unlike user IDs (which can skew if one user creates millions of URLs) or long URLs (which vary wildly in length and distribution), short codes are fixed-length and drawn from a large space, so they hash evenly.
The short code is the ideal partition key because it is the primary lookup key (every redirect uses it), it is drawn from a large uniform space (even distribution after hashing), and it is fixed-length (consistent hashing behavior).
Why not partition by long URL or user ID? Long URLs vary wildly in length and structure: some domains (youtube.com, twitter.com) would concentrate on a few partitions while others have very few entries. User ID partitioning creates hotspots when a single power user generates millions of URLs. Short codes avoid both problems because they are drawn from a large, evenly distributed code space.
Mid-level: explain Base62 encoding and why random codes prevent enumeration.
Senior: describe the collision handling mechanism with conditional writes, explain range-based ID allocation, and discuss cache-aside vs write-through trade-offs.
Staff: analyze the birthday paradox collision probability, design a pre-generated code pool with background replenishment, and explain how to handle hot keys that overwhelm a single cache shard.
Hash vs Random: A Deeper Look
The hash approach (hash the long URL, truncate to 7 characters) offers one compelling advantage: determinism. The same long URL always produces the same short code, which provides natural deduplication. If a user submits a URL that was already shortened, the system returns the same short code without a database write.
The downside is severe. Truncating a hash reduces the output space dramatically. A full SHA-256 hash has 2 to the 256th power possible outputs, essentially zero collision risk. Truncating to 7 Base62 characters reduces this to 62 to the 7th power (about 3.5 trillion). While 3.5 trillion sounds large, the birthday paradox means the probability of at least one collision reaches 50% at roughly 2 million entries. For a system storing hundreds of millions of URLs, truncated-hash collisions become a regular operational concern.
The random approach avoids the truncation problem because it uses the full 7-character space uniformly. Collision probability is lower for the same number of stored entries. The trade-off: you lose deterministic deduplication and must use a secondary index (GSI on longUrl) to check for existing mappings.
Random generation with conditional-write collision handling is the simplest approach, but it has a critical limitation: if the system returns the code to the client before the database write (as in an async queue pattern), a collision means the client holds a code that maps to someone else's URL. In practice, range-based counter allocation is the safest choice for systems that use async writes. It eliminates collisions entirely and enables the queue-based write pattern without correctness risks.
To quantify the difference: with 500 million stored URLs and 7 Base62 characters, the hash-truncation approach produces a collision roughly once every 7,000 new URLs (due to birthday paradox effects on the truncated space). Random generation in the same scenario produces a collision roughly once every 7,000 new URLs as well. The probability is similar because both use a 7-character output space. The difference is that hash truncation concentrates collisions on specific inputs (URLs whose hashes share a prefix), while random generation distributes collision risk uniformly.
When discussing hash or random generation alternatives in an interview, use concrete numbers to quantify collision risk. With 7 Base62 characters (3.5 trillion possible values), the birthday paradox predicts a 50% collision probability at roughly 2 million entries. With 500 million stored URLs, the collision rate per new code is approximately 1 in 7,000. With range-based allocation (the chosen approach), collision risk is zero, but these numbers are useful for comparing approaches and explaining why range-based was chosen.
Hash truncation is not a free operation. It exponentially increases collision probability. A full SHA-256 hash (256 bits) has negligible collision risk, but truncating to 7 Base62 characters (about 41 bits) reduces the space by a factor of 2 to the 215th power. Every bit removed from the hash doubles the collision rate. Always calculate the collision probability for your truncated length before committing to this approach.
301 vs 302: Revisited
This trade-off has a nuanced middle ground. Using 302 by default preserves analytics and flexibility. But you can offer 301 as an opt-in for users who want maximum performance and do not need click tracking. The CDN also helps bridge the gap: it caches 302 responses at edge locations with a short TTL, giving you 301-like load reduction while preserving the flexibility to update destinations or enforce expiration.
Database Choices
DynamoDB: Fully managed, auto-scales, single-digit-millisecond reads. The trade-off is vendor lock-in to AWS. If your company requires multi-cloud portability, DynamoDB is off the table.
Cassandra: Open-source, cloud-portable, excellent write throughput. The trade-off is significant operational complexity: you must manage compaction, repairs, cluster topology, and tuning. Cassandra excels when you need multi-cloud or on-premises deployment.
PostgreSQL: ACID transactions, familiar SQL interface. Works well for systems under 5,000 reads per second with moderate data volumes. Beyond that, you need manual sharding, connection pooling, and read replicas, operational work that DynamoDB handles automatically.
For most teams building a URL shortener, DynamoDB is the pragmatic choice unless vendor lock-in is a hard constraint.
Pre-Generated Codes vs On-Demand
Pre-generated pool: A background job fills a table of unused codes. When a shortening request arrives, grab a code from the pool. Zero collision risk at request time. The trade-off: added complexity (pool management, replenishment, ensuring codes are not reused across servers).
On-demand generation: Generate a code at request time. With range-based allocation, the code is guaranteed unique from the server's exclusive ID range — no collision check needed. With random generation, collisions must be handled via conditional write retry, which requires a synchronous database write before responding to the client.
For systems using async writes via a message queue, range-based allocation is the right choice because it guarantees uniqueness at generation time. Random generation works for simpler systems with synchronous writes, where the conditional write happens before the client receives the code. Pre-generated pools offer a middle ground: zero collision risk without the coordination service that range-based allocation requires.
Cache Stampede
When a popular URL's cache entry expires in Redis, or a Redis node fails, thousands of concurrent requests for that URL simultaneously miss the cache and all hit DynamoDB at once. This sudden load spike can overwhelm the database, causing slow responses or throttling errors. The slow responses cause timeouts in the Mapping Service, which retries, sending even more requests to the database, creating a cascading failure.
Mitigations: Use Redis cluster mode with replicas so a node failure triggers automatic failover rather than a total cache loss. Implement cache entry locking (only one request fetches from DynamoDB while others wait for the cache to be repopulated). Set staggered TTLs with jitter so cache entries do not expire simultaneously.
Hot Key Problem
A celebrity shares a short URL, generating millions of clicks per second on a single key. Even with caching, a single Redis shard holding that key can be overwhelmed. DynamoDB throttles reads on a single partition beyond its per-partition limit.
Mitigations: Replicate hot keys across multiple Redis nodes (read from any replica). The CDN absorbs the majority of traffic at edge locations before it reaches Redis. Implement a small local in-memory cache in each Mapping Service instance for the hottest keys, using a very short TTL (seconds) to limit staleness.
Write Spikes
A viral event triggers mass URL creation, with thousands of users simultaneously shortening the same trending article. Without the message queue, these writes would hit DynamoDB directly, exceeding provisioned capacity and causing throttling errors.
Mitigation: The message queue absorbs the spike. The Queue Worker drains at a steady rate the database can handle. A burst of 2,000 writes per second is smoothed to a steady 200 writes per second. The queue depth may grow temporarily, but no writes are lost. Monitor queue depth and set alarms for sustained growth.
Data Durability
DynamoDB replicates data across three availability zones by default, protecting against single-AZ failures. For disaster recovery, enable point-in-time recovery (PITR) for continuous backups and periodic snapshots to S3 in a separate region.
Redis data loss during failover is acceptable because Redis is a cache, not the source of truth. The data can always be reloaded from DynamoDB on the next cache miss. The key design principle: never rely on cache for durability. If every Redis node disappeared simultaneously, the system would be slow (all requests hit DynamoDB) but no data would be lost.
Database Partition Throttling
DynamoDB distributes data across partitions, with each partition supporting up to 3,000 read capacity units and 1,000 write capacity units per second. If a single short code receives extreme traffic (a viral URL), the partition holding that code can be throttled even if the table has plenty of total capacity. This is distinct from the hot key problem at the cache layer. It is a database-level concern.
Mitigation: Use DynamoDB adaptive capacity, which automatically redistributes provisioned capacity to hot partitions. For extreme cases, use DynamoDB on-demand mode, which eliminates the concept of provisioned capacity entirely and scales to any traffic pattern. The trade-off is higher per-request cost compared to provisioned mode.
Monitoring Essentials
Track these key metrics to catch problems early:
- Cache hit rate: should stay above 80%. A sudden drop indicates a Redis node failure or a change in traffic pattern.
- DynamoDB consumed capacity: compare against provisioned capacity. If consumed approaches provisioned, auto-scaling or manual increase is needed before throttling starts.
- Queue depth and processing lag: sustained growth means the Queue Worker cannot keep up with incoming writes. Scale up workers or investigate slow writes.
- Redirect latency at the 50th and 99th percentiles: p50 should be under 10ms (cache hits), p99 under 100ms (DB fallback). A rising p50 indicates cache degradation.
- Error rates per endpoint: any spike above baseline warrants immediate investigation.
Set alerts on all of these metrics. Early detection prevents most cascading failures because you can intervene before a small problem (one Redis node down) becomes a large one (database overwhelmed by cache misses).
Custom Alias Enhancements
The core API already supports basic custom aliases (the customAlias parameter in shortenURL). This future improvement adds namespace management features on top: a profanity filter, trademark screening, a first-come-first-served reservation model, and an admin dashboard for alias disputes. The same DynamoDB table stores custom aliases alongside range-generated codes, using conditional writes to prevent alias conflicts.
Click Analytics
Track every redirect: timestamp, referrer, user agent, geolocation (derived from IP). The key constraint is that analytics collection must not add latency to the redirect path. Architecture: each redirect fires an asynchronous event to a stream (Kafka or Kinesis). A separate analytics pipeline consumes the stream and writes to a time-series database (ClickHouse or TimescaleDB). A dashboard queries aggregated data. This is a fire-and-forget pattern: if an analytics event is dropped, the redirect still succeeds.
TTL Auto-Deletion
DynamoDB TTL automatically deletes expired items in the background at no additional read or write cost. For Redis, set the cache entry TTL to match the URL expiration. Benefits: recovers storage space, reduces cache pollution with expired entries, and ensures expired URLs return 404 instead of redirecting to potentially stale destinations.
Abuse Prevention
URL shorteners are frequently used to disguise phishing links, malware distribution, and spam. A malicious URL behind tinyurl.com/abc123 looks identical to a legitimate one, and the user cannot tell the difference before clicking. Integration with Google Safe Browsing API or a similar reputation service at creation time can flag or block known-bad destinations. Real-time monitoring for suspicious patterns (mass creation from one IP, known-bad destination domains, rapid-fire submissions) adds another layer of defense.
Link Preview and Metadata
When short URLs are shared on social media or messaging platforms, users see a preview card (Open Graph tags from the destination page). The shortener can enhance this by fetching and caching the destination page's metadata (title, description, image) at creation time. When a platform requests metadata for a short URL, the shortener returns the cached preview without requiring the platform to follow the redirect. This improves the sharing experience and reduces load on destination servers.
Multi-Region Deployment
For a global user base, deploy the system across multiple AWS regions. DynamoDB Global Tables provide automatic multi-region replication with single-digit-millisecond reads in any region. Redis clusters can be deployed per-region with independent data sets (since the cache is rebuilt from DynamoDB on misses). The CDN already operates globally by design. Multi-region deployment reduces redirect latency for international users and provides geographic redundancy for disaster recovery.
Try this problem yourself
Reading a solution is not the same as producing one under interview pressure. Design it on the whiteboard and get scored feedback.
Asked in these interview guides
Company guides that list this problem among their sample questions.
0%
Editorial Solution
System requirements
Functional
Non-Functional
Scope Control
Capacity estimation
Traffic
Storage
Bandwidth
Cache Sizing
Why These Numbers Matter
API design
Create Short URL
Redirect
301 vs 302: Why It Matters
Rate Limiting
Database design
Why a Key-Value Store
Schema
Secondary Indices
Why Not PostgreSQL
Why Not Redis Alone
Consistency Model
High-level design
Components
Why This Component Split Works
Request flows
Write Flow (shortenURL)
Read Flow (redirectURL)
Latency by Tier
Detailed component design
Short Code Generation
Collision Handling with Conditional Writes
Caching Strategy
Partitioning
Trade offs/Tech choices
Hash vs Random: A Deeper Look
301 vs 302: Revisited
Database Choices
Pre-Generated Codes vs On-Demand
Failure scenarios/bottlenecks
Cache Stampede
Hot Key Problem
Write Spikes
Data Durability
Database Partition Throttling
Monitoring Essentials
Future improvements
Custom Alias Enhancements
Click Analytics
TTL Auto-Deletion
Abuse Prevention
Link Preview and Metadata
Multi-Region Deployment