A URL shortener looks like one key-value lookup. A production design also has to make creation retries safe, prevent code collisions, revoke abusive links, survive cache failure, and preserve low redirect latency across regions. Those constraints make it a useful senior system design interview problem.
This walkthrough applies the six-step system design framework and makes every important assumption explicit. The numbers are a sizing scenario, not claims about every shortener.
Table of Contents
- Clarify the Contract
- Estimate Scale
- Design the API and Data Model
- Build the High-Level Architecture
- Generate Codes Safely
- Cache Without Losing Revocability
- Handle Failure and Multiple Regions
- Security, Abuse, and Analytics
- Common Interview Mistakes
- Quick Reference
- Frequently Asked Questions
Step 1 - Clarify the Contract
Start with product semantics because they determine the cache and consistency design.
Core requirements:
- create a short link for an
httporhttpsdestination; - redirect a code to its destination;
- optionally accept a custom alias and expiration time;
- disable a link for its owner, policy enforcement, or abuse response;
- keep link creation idempotent across client retries;
- emit click events without delaying the redirect.
Ask whether two requests for the same destination should create one shared link or two independent links. Shared deduplication saves storage but conflicts with per-link ownership, expiration, campaigns, and analytics. A strong default is one link per logical creation request, with an idempotency key making retries of that request return the same result.
Clarify whether destinations can be edited. This design treats them as stable after activation but still supports expiration and disabling. Editing can be added as a versioned policy change; it should not be hidden behind the claim that mappings are immutable.
Non-functional requirements:
- low redirect latency and high read availability;
- a defined revocation propagation objective, for example one minute globally;
- no trivial enumeration of private or unlisted links;
- explicit regional RPO and RTO;
- bounded degradation when cache, store, or control-plane components fail.
Step 2 - Estimate Scale
Use an interview scenario of 100 million creations and 10 billion redirects per day:
| Signal | Average | Example peak assumption |
|---|---|---|
| Creations | about 1,160/s | 6,000/s |
| Redirects | about 116,000/s | 500,000/s |
The 100:1 ratio and 5x peaks are assumptions to validate with traffic data. Edge caching can make the request rate seen by the origin much lower than the end-user redirect rate.
If a logical record averages 500 bytes, new mappings alone consume about 50 GB/day or 18 TB/year. Indexes, replicas, change logs, backups, allocator metadata, and analytics are additional capacity. Treat 500 bytes as a lower-bound input to a storage model, not the provisioned footprint.
For a case-sensitive base62 alphabet:
| Length | Namespace | Time to consume at 100M new links/day |
|---|---|---|
| 6 | 62^6 = 56.8 billion | about 1.6 years |
| 7 | 62^7 = 3.52 trillion | about 96 years |
| 8 | 62^8 = 218 trillion | about 5,980 years |
Seven characters are a reasonable starting point, but usable capacity is lower after reserving routes and aliases. Code length is not literally irreversible: old seven-character links can coexist with newly generated eight-character links if routing and parsers accept both. Plan that migration before the namespace becomes crowded. Also define case sensitivity deliberately; clients or intermediaries that fold case can break a base62 scheme.
Step 3 - Design the API and Data Model
Creation should distinguish a new logical request from a network retry.
POST /api/links
Idempotency-Key: 0a6c...
Content-Type: application/json
{
"destination": "https://example.com/product?id=42",
"customAlias": "launch-2026",
"expiresAt": "2027-01-01T00:00:00Z"
}
201 Created
{
"code": "aZ3kP9x",
"shortUrl": "https://sho.rt/aZ3kP9x",
"status": "ACTIVE"
}Scope the idempotency key to the caller and endpoint, and store a request hash. Reusing the key with different input must fail; repeating the same input returns the original result. Persist the mapping, idempotency record, and publication event atomically when the chosen store supports it.
The redirect endpoint returns a status selected by product policy:
GET /aZ3kP9x
302 Found
Location: https://example.com/product?id=42
Cache-Control: public, max-age=0, s-maxage=60, must-revalidateThe example lets a shared cache hold the result briefly while keeping browser freshness at zero. The correct values depend on the revocation objective and CDN behavior. 302 is not a mechanism that guarantees every click reaches the origin.
A minimal authoritative record contains:
| Field | Purpose |
|---|---|
short_code | Unique lookup key and route identifier |
destination | Exact validated destination; avoid semantic over-normalization |
status | ACTIVE, DISABLED, or EXPIRED |
version | Monotonic policy/cache version |
owner_id | Ownership and quota boundary |
created_at | Audit timestamp |
expires_at | Optional absolute expiration |
disabled_at, reason | Revocation audit trail |
redirect_policy | Temporary/permanent and cache policy |
A key-value store is natural for exact lookups, but a relational store can also satisfy the workload with sharding and replicas. Select the database from measured throughput, conditional-write semantics, replication, operations, and cost—not from the shape of one query alone. Hash partition randomized codes; sequential allocator ranges must not create a single hot partition.
Step 4 - Build the High-Level Architecture
Separate the latency-critical redirect data plane from the creation and policy control plane.
flowchart TD
Client([Client])
Edge[CDN / edge cache]
Redirect[Redirect service]
Local[In-process cache]
Cache[(Distributed cache)]
Store[(Authoritative mapping store)]
Create[Create-link service]
Idem[(Idempotency records)]
Generator[Code generator / allocator]
Outbox[(Outbox / change stream)]
Control[Cache purge and policy consumers]
Events[(Bounded analytics stream)]
Client -->|GET /code| Edge
Edge -->|miss| Redirect
Redirect --> Local --> Cache --> Store
Redirect -.non-blocking event.-> Events
Edge -.edge event.-> Events
Client -->|POST /api/links| Create
Create --> Generator
Create --> Store
Create --> Idem
Create --> Outbox --> Control
Control -.purge / version update.-> Edge
Control -.invalidate.-> Local
Control -.invalidate.-> CacheFigure 1. Redirect traffic uses layered caches backed by an authoritative store. Creation and policy changes publish invalidation events, while analytics stays off the synchronous redirect path.
The store—not a successfully warmed cache—is the source of truth. A cache write after creation is useful best effort, but it cannot guarantee read-after-create across every process, edge, and region. If immediate resolution is contractual, route the first read to the authoritative region, use a consistency token, or perform a strong lookup before concluding the code is missing.
Step 5 - Generate Codes Safely
There are two strong designs; choose based on security, density, and operational constraints.
Option A: CSPRNG code plus conditional insert
Generate seven or eight characters with a cryptographically secure random number generator, then issue an atomic “insert if absent.” Retry on conflict. With a namespace M and n occupied codes, the approximate collision probability for the next independent attempt is n/M. At 100 million occupied seven-character codes, that is about 0.0028%, so retries are cheap early in the namespace's life. Monitor occupancy and retry rates rather than invoking the birthday bound without connecting it to the operation being designed.
Option B: range allocation plus reviewed keyed permutation
An allocator leases each writer a disjoint numeric range. Writers consume it locally and can continue while the allocator is temporarily unavailable.
sequenceDiagram
participant C as Create service
participant A as Range allocator
participant S as Authoritative store
C->>A: reserve range for writer epoch
A-->>C: [4,500,000..4,509,999], lease metadata
loop each creation
C->>C: next ID, keyed permutation, base62
C->>S: conditional insert mapping + idempotency record
S-->>C: committed or conflict
endFigure 2. Range allocation amortizes coordination. The store still enforces uniqueness, and unused IDs can be abandoned safely after a crash.
Never expose the raw counter. A reviewed keyed permutation can deter casual enumeration, but it is not authentication and custom cryptography is a poor substitute for a CSPRNG design. If link secrecy matters, use enough random entropy and still require authorization for protected content—the destination must not rely on an unguessable URL as its only access control.
Custom aliases need normalization rules, reserved-name checks, ownership limits, and the same atomic conditional insert. Decide whether aliases are case-sensitive before launch. Generated and custom namespaces must not collide.
Step 6 - Cache Without Losing Revocability
Mappings are stable, but not immutable. Expiration, deletion, abuse takedown, account suspension, and policy changes all affect the redirect result. Cache entries therefore carry the mapping version and an expiry no later than both the link expiration and the revocation-staleness objective.
flowchart TD
Start([GET /code]) --> E{Fresh edge entry?}
E -->|yes| Redirect[Return configured redirect]
E -->|no| C{Fresh local or distributed entry?}
C -->|yes| Policy{ACTIVE and not expired?}
C -->|no| Single[Collapse concurrent misses]
Single --> S{Authoritative lookup}
S -->|ACTIVE| Fill[Fill caches with bounded TTL] --> Redirect
S -->|DISABLED / EXPIRED| Gone[Return 410 with short explicit TTL]
S -->|unknown| Negative[Cache negative result briefly] --> Missing[Return 404]
Policy -->|yes| Redirect
Policy -->|no| GoneFigure 3. Cache-aside remains effective, but status, version, TTL, purge events, and miss collapsing preserve revocability and protect the store.
Negative caching protects against random-code scans, but it creates a read-after-create trap: a code or custom alias that previously returned 404 can remain hidden. Keep negative TTLs short and invalidate the negative key when creation commits. If generated codes are never reused, negative entries are safer for that namespace than for user-selectable aliases.
HTTP status and caching must be designed together:
301and308represent permanent moves and are heuristically cacheable. A client may retain them beyond your operational control.302and307represent temporary redirects; explicitCache-Controlstill determines whether a cache may reuse them.301and302historically permit a user agent to change a POST to GET.307and308preserve the method. Most short links are GET-only, but the distinction matters when discussing general redirect semantics.- use
404for an unknown code and410when the resource is intentionally unavailable; give both an explicit, bounded cache policy because error responses can also be cached.
For revocable links, a temporary redirect with a short shared-cache TTL is a safer default than a permanent browser-cacheable redirect. CDN or server-side event collection can observe cache hits; origin access logs alone cannot count every click. Browser caches can make exact click totals impossible.
Step 7 - Handle Failure and Multiple Regions
Cache or hot-key failure
A viral code should be served at the edge and from small per-process caches before it reaches a distributed cache shard. Replicate a hot entry or use a cache design that distributes reads, and collapse concurrent misses. During a cache outage, admission control and bulkheads limit store traffic; “all requests fall through” is not a survivability plan unless the store was explicitly sized and tested for that load.
Allocator or store failure
Range-based writers continue until their current ranges are exhausted; random-code writers do not need an allocator. Both still need the authoritative store to commit a link. If the write store is unavailable, fail creation cleanly or queue only when the product accepts delayed visibility and the idempotency contract survives replay.
Redirect behavior during store failure depends on policy. Fresh cached entries can continue. Serving stale entries improves availability but can violate a takedown or expiration objective, so define which statuses may be served stale and for how long. Security-sensitive revocations may require fail-closed behavior or a home-region check.
Multi-region authority
Asynchronous replication introduces correctness risks: a fresh link may 404 remotely, a disabled link may continue redirecting, and a custom alias can be claimed concurrently. Resolve each explicitly:
- generated codes use disjoint regional ranges/prefixes, or a globally conditional random-code write;
- custom aliases are created through one home or globally consistent authority;
- a miss can route to the code's home region before returning 404;
- revocation publishes purge/version events and is bounded by the maximum cache TTL;
- allocator epochs or store fencing stop stale writers after ownership changes;
- conflict policy, RPO, RTO, failover, and restoration are tested rather than implied by “active-active.”
flowchart LR
subgraph EU[EU region]
ECreate[Create authority]
ERead[Redirect service]
EStore[(EU store)]
end
subgraph US[US region]
UCreate[Create authority]
URead[Redirect service]
UStore[(US store)]
end
Alias[Custom-alias authority]
Purge[Global purge / version channel]
ECreate --> EStore
UCreate --> UStore
ECreate --> Alias
UCreate --> Alias
EStore -.replication.-> UStore
UStore -.replication.-> EStore
ERead --> EStore
URead --> UStore
ERead -.home fallback.-> UStore
URead -.home fallback.-> EStore
Alias --> Purge
ECreate --> Purge
UCreate --> PurgeFigure 4. Regional creation needs disjoint generated namespaces, while custom aliases use a single uniqueness authority. Home-region fallback avoids turning ordinary replication lag into a false 404.
Security, Abuse, and Analytics
Accept only parsed http and https URLs. Reject control characters, invalid encodings, excessive length, and values that could produce response-header injection. Preserve destination semantics: broad “canonicalization” such as changing case, query order, or trailing slashes can change the resource.
A shortener can hide phishing and malware. Add authentication where appropriate, per-owner and per-network quotas, abuse reporting, risk scanning, disable/takedown workflows, and audit logs. Any server-side preview, unfurl, or safety fetch is an SSRF surface: run it in an isolated fetcher with egress policy, DNS/IP checks, redirect limits, timeouts, and response-size limits.
Emit analytics asynchronously from the edge and origin into a bounded stream. Sampling, bot classification, retention, consent, and deletion policy belong in the design. If the event system is slow, drop or spool within a strict bound rather than delaying redirects. State whether metrics are approximate; caching, blockers, retries, and bots make “exact unique clicks” a product definition, not a raw counter.
Monitor redirect latency by cache tier, hit ratio, origin QPS, conditional-write conflicts, idempotency replays, negative-cache hits, replication lag, purge propagation, allocator range burn, home-region fallbacks, abuse decisions, and event loss. Alert on user-visible SLOs and exhausted safety margins rather than on component CPU alone.
Common Interview Mistakes
- Claiming that mappings are immutable and therefore never require invalidation.
- Saying
302forces every click through the origin or that301is always the fastest choice. - Warming one cache after creation and calling global read-after-write consistency solved.
- Using a raw counter or home-grown Feistel network as a security control.
- Applying the birthday bound vaguely instead of calculating the next-insert collision rate and namespace occupancy.
- Negative-caching 404s without invalidating a newly created alias.
- Assuming asynchronous multi-region replication has no correctness cost.
- Letting analytics, preview fetching, or abuse checks block the redirect data plane.
- Sending all traffic to the store during cache failure without admission control or tested capacity.
- Treating scenario estimates such as 100:1 traffic and 500-byte records as measured facts.
Quick Reference
| Decision | Defensible default |
|---|---|
| Creation retry | Caller-scoped idempotency key plus request hash |
| Generated code | CSPRNG + conditional insert, or leased range + reviewed permutation |
| Alias uniqueness | Reserved-name validation and one conditional-create authority |
| Redirect | Temporary status with explicit bounded cache policy for revocable links |
| Source of truth | Authoritative mapping/status store; cache is disposable |
| Cache | Edge + local + distributed; version, TTL, purge, negative invalidation |
| Hot miss | Request collapsing, admission control, bulkheads |
| Multi-region | Generated namespace ownership, alias authority, home fallback, fencing |
| Analytics | Bounded asynchronous events from edge and origin |
| Abuse | Quotas, scanning, reports, auditable disable and rapid purge |
Frequently Asked Questions
How do you generate short, unique URL codes at scale?
Use a cryptographically secure random code plus an atomic conditional insert, or allocate disjoint numeric ranges and transform IDs with a reviewed keyed permutation before base62 encoding. A 7-character base62 namespace has about 3.52 trillion values, but reserved aliases and current occupancy still matter. Keep a uniqueness constraint and retry collisions; custom aliases need the same conditional-create rule.
Should a URL shortener use sequential IDs?
Sequential IDs are useful internally because allocation is compact and collision-free, but exposing them directly enables enumeration and leaks growth. A keyed permutation can obscure a counter from casual observers, but it is not authorization and should not be home-grown cryptography. A CSPRNG-generated public code is often the simpler security choice.
Why is a URL shortener a good fit for cache-aside?
Redirect lookup is read-heavy and mappings are mostly stable, so cache-aside is effective. They are not truly immutable: links can expire or be disabled for abuse, and policy can change. Cache TTLs must therefore be bounded by the expiry and revocation objective, with purge or versioned invalidation for urgent changes.
Should redirects use HTTP 301, 302, 307, or 308?
Use 301 or 308 only when the destination is genuinely permanent; these responses can be cached heuristically and may be difficult to revoke once stored by a client. For revocable links, 302 or 307 with explicit Cache-Control is safer. A 302 does not guarantee every click reaches the origin, and 307 or 308 should be used when preserving a non-GET method matters.
How do you handle a viral link that becomes a hot key?
Serve bounded-fresh redirects from the CDN, add a small in-process cache, replicate or shard hot cache entries, and collapse concurrent misses so one lookup reaches the store. Protect the store with admission control and bulkheads during cache failure; do not assume it can absorb the entire edge traffic rate.
How read-heavy is a URL shortener?
The ratio is workload-specific; 100 redirects per creation is a useful interview assumption, not a universal fact. Measure edge, origin, and creation traffic separately because CDN and browser caching change what the origin observes. Size each tier from its own peak rate, hit ratio, and failure scenario.
Sources
- RFC 9110: HTTP Semantics - redirect status semantics, method preservation, and heuristic cacheability.
- RFC 9111: HTTP Caching - freshness,
Cache-Control, revalidation, and collapsed forwarding. - RFC 3986: Uniform Resource Identifier Syntax - URI parsing and normalization boundaries.
Related Articles
- System Design Interview Problems: A Senior's Roadmap - the series index and pattern library.
- System Design Interview Guide: The 6-Step Framework - the method used here.
- Design a Rate Limiter - distributed limits, atomic updates, and failure policy.
- Design a Unique ID Generator - timestamp, range, and UUID-based ID trade-offs.
- Redis Interview Questions - cache consistency, eviction, persistence, and failure behavior.
This is Part 1 of a 16-part system design series. Next: Design a Rate Limiter.
