Design a URL Shortener on AWS
A worked system-design example mapping the classic 'design TinyURL' interview question onto real AWS services — DynamoDB for the core lookup, API Gateway/Lambda for the API, and CloudFront/Route 53 for global low-latency redirects.
Want a visual for this topic?
Generate a diagram tailored to Design a URL Shortener on AWS — the AI picks whichever visual (architecture, flowchart, ER diagram, etc.) best fits this specific AWS concept.
Sign in to generate a visual →🎓 Learning objectives
- •Map the URL shortener's core read/write pattern onto a specific AWS database choice and justify it
- •Explain how unique short-code generation avoids collisions at scale without a central bottleneck
- •Design the redirect path for minimum latency using CloudFront and DynamoDB
- •Identify what AWS services would be overkill or the wrong fit for this specific problem
What is it?
A URL shortener system design maps the classic interview problem — accept a long URL, return a short code, and redirect any request for that short code back to the original long URL — onto concrete AWS services chosen specifically for this access pattern: DynamoDB as the core key-value store for short-code lookups, API Gateway + Lambda for the create/redirect API (or a lightweight container service), CloudFront caching hot redirects at the edge, and Route 53 providing the custom short domain's DNS.
Why it exists
This is a canonical system-design interview question specifically because its simplicity forces a candidate to demonstrate they understand the core system-design skill of matching a service choice to an access pattern — recognizing 'this is a simple, massive-scale key-value read problem' and choosing DynamoDB+caching accordingly, rather than defaulting to a relational database and then fighting to scale it.
Problem it solves
It solves reliably converting long URLs into short, memorable codes and redirecting billions of subsequent requests back to the original URL with minimal latency, at a scale and read-to-write ratio where a naive relational-database-backed approach would eventually become a scaling bottleneck.
Intuition
The entire design boils down to optimizing one specific, extremely simple read pattern (short code → long URL) for maximum scale and minimum latency, while keeping the write path (creating a short URL) simple and collision-safe — nearly every service choice here follows directly from recognizing that this is fundamentally a key-value lookup problem at very high read volume, not a problem needing relational joins, complex queries, or strong consistency guarantees.
Analogy
A URL shortener is like a coat-check counter at a huge venue: you hand over your coat (the long URL) once and get back a small numbered ticket (the short code); every time someone shows that ticket, the counter needs to hand back the exact right coat instantly, no matter how many millions of tickets are in circulation — which is exactly the kind of simple, high-volume, key-based lookup DynamoDB is built for.
Technical explanation
A conditional write in DynamoDB (PutItem with ConditionExpression: attribute_not_exists(shortCode)) atomically checks-and-inserts in one operation, meaning even under concurrent requests generating the same random short code, only one write succeeds and the other receives a ConditionalCheckFailedException to retry with a new code — this avoids needing any separate distributed locking mechanism for collision safety. CloudFront's cache behavior for the redirect path can be configured with a Cache-Control header set by the Lambda origin response, giving fine-grained control over how long a given redirect stays cached at the edge versus falling through to re-validate against the origin.
Architecture
Route 53 resolves the custom short domain to a CloudFront distribution; CloudFront serves cached redirects directly from edge locations for popular short codes, and forwards cache misses to an API Gateway REST/HTTP API backed by Lambda functions for both the create and redirect paths; both functions read/write a single DynamoDB table keyed on short code, with the create path additionally using a conditional write to guard against code collisions. A separate, asynchronous pipeline (API Gateway/Lambda → Kinesis Data Streams → a downstream aggregation Lambda or analytics service) processes click events without touching the synchronous redirect path.
Workflow
- Client submits a long URL to a
POST /shortenendpoint (API Gateway → Lambda). 2) Lambda generates a short code (random base62 string or a sharded counter) and writes{shortCode, longUrl, createdAt}to DynamoDB using a conditional write to detect collisions. 3) The short URL is returned to the client. 4) On a laterGET /{shortCode}request, CloudFront checks its cache; on a miss, it forwards to API Gateway → Lambda, which performs a DynamoDBGetItemon the short code and returns an HTTP redirect to the long URL, which CloudFront caches for subsequent requests. 5) Click events are asynchronously pushed to Kinesis/SQS for separate analytics aggregation, off the hot redirect path.
Example
A user submits a long URL via a POST request; a Lambda function generates a random 7-character base62 short code, attempts a conditional write to DynamoDB (retrying on the rare collision), and returns short.ly/aB3xQ9. When anyone later visits that short URL, CloudFront checks its edge cache first — a cache hit redirects instantly with no origin call at all; a cache miss falls through to API Gateway → Lambda → a single DynamoDB GetItem call, returning a 301/302 redirect to the original long URL.
Real-world usage
This exact pattern — key-value store plus edge caching plus a lightweight serverless API — is used by real production URL shorteners and is a frequently-asked system-design interview question precisely because it's simple enough to fully design in an interview's time limit while still testing genuine service-selection judgment.
Trade-offs
Using a random short code with conditional-write collision retry is simpler to implement and reason about than a distributed counter-sharding scheme, at the cost of a small (but manageable, and shrinking as code length increases) chance of needing a retry on write. Caching redirects aggressively at CloudFront trades a small staleness window (a long URL updated after creation won't be reflected in a still-cached redirect until the cache TTL expires) for dramatically reduced origin load and latency — an acceptable tradeoff since short URLs are essentially immutable once created in almost every real product.
Visual explanation
Picture a fast-food drive-through with an express lane (CloudFront cache) for regular customers' usual orders, and a regular window (API Gateway → Lambda → DynamoDB) for anything not already memorized — the vast majority of traffic (popular links) never needs to reach the kitchen (DynamoDB) at all.
Advantages
- —
DynamoDB's key-based lookups scale near-linearly with almost no operational tuning, matching the read-heavy access pattern directly
- —
CloudFront edge caching absorbs the bulk of traffic for popular links before it ever reaches the origin, keeping both latency and origin cost low
- —
Lambda's pay-per-request billing fits a workload with unpredictable, bursty traffic without needing to provision for peak capacity
- —
Decoupling click-analytics from the redirect path (via Kinesis/SQS) keeps the latency-critical path simple and fast
Disadvantages
- —
Aggressive CloudFront caching means a long URL can't be practically changed/deleted with immediate effect across all edge locations — cache invalidation adds complexity if that requirement exists
- —
A purely random short-code approach needs collision-retry logic, adding a small amount of complexity (and rare extra latency) to the write path compared to a strictly sequential ID
- —
DynamoDB's simplicity for key lookups doesn't extend to complex analytics queries (e.g., 'top 10 links this month') — those need a separate analytics pipeline/store, not ad-hoc queries against the operational table
- —
A custom short domain requires its own domain registration, ACM certificate, and Route 53/CloudFront setup — a small but real amount of upfront infrastructure beyond just the application logic
Common mistakes
- —
Defaulting to RDS/Aurora for the core short-code lookup out of habit, when the access pattern is a textbook fit for a simpler, more scalable key-value store
- —
Synchronously incrementing a click counter on every redirect, adding write latency and a hot-partition risk to the latency-critical read path
- —
Using a single global auto-incrementing counter for short-code generation, creating an unnecessary write bottleneck and a single point of contention at scale
- —
Not putting a CDN in front of the redirect path at all, missing the biggest, cheapest latency and origin-load win available for this specific access pattern
In the AWS Console
- 1
DynamoDB → Tables → Create table
Create a DynamoDB table with `shortCode` as the partition key.
- 2
Lambda → Create function, then API Gateway → Create API → HTTP API
Create the Lambda functions for create/redirect and wire them behind an API Gateway HTTP API.
- 3
CloudFront → Distributions → Create distribution
Create a CloudFront distribution in front of the API Gateway endpoint, with caching enabled for the redirect route.
🎤 Interview questions
Why is DynamoDB a better fit than RDS for a URL shortener's core short-code-to-long-URL mapping? (Listen for: the access pattern is a simple key-value lookup — get long URL by short code — with extremely high read volume and simple write patterns; DynamoDB's single-digit-millisecond key lookups and effectively unlimited horizontal scaling fit this far better than a relational database, which would need read replicas and careful indexing to reach comparable scale for what is fundamentally a key-value problem)
How would you generate short codes at scale without collisions, without a single bottleneck service handing out codes one at a time? (Listen for: options include base62-encoding an auto-incrementing ID sharded across ranges pre-allocated to each writer (avoiding a single counter bottleneck), or generating a random 7-character code and doing a conditional-write (ConditionExpression: attribute_not_exists) to DynamoDB, retrying on collision — the second approach is simpler and collision probability at 7 base62 characters is astronomically low, making retries rare)
How do you make the redirect (read) path as fast as possible for a user clicking a short link? (Listen for: put CloudFront in front of the redirect endpoint with a short TTL cache for frequently-accessed short codes, so hot links are served straight from a CloudFront edge location without hitting the origin at all; for cache misses, API Gateway + Lambda + DynamoDB's single-digit-ms lookup still keeps the uncached path fast)
Would you use Lambda or a container-based compute (ECS) for this workload, and why? (Listen for: Lambda is a strong fit — the workload is simple, stateless, bursty, and read-heavy, which plays well to Lambda's auto-scaling and pay-per-request model; a small always-on ECS service would work too, but adds idle-capacity cost for a workload that doesn't need it, especially compared to Lambda combined with CloudFront caching soaking up most read traffic before it even reaches a Lambda invocation)
How would you handle analytics (click counts) for each short link without slowing down the redirect path itself? (Listen for: don't synchronously update a click counter on every redirect request — that adds write latency to the hot path and risks a hot-partition issue in DynamoDB for very popular links; instead, asynchronously stream click events (via Kinesis Data Streams or an SQS queue) to be aggregated separately, keeping the redirect path itself a simple, fast read)