Design a URL shortener
A URL shortener takes a long URL and returns a shorter, unique alias that redirects to the original address. Services like TinyURL and Bitly became essential when social posts had character limits and when teams wanted cleaner, trackable links. In a system design interview, this question tests whether you can clarify requirements, estimate scale, separate read and write paths, and defend storage and caching choices under pressure.
This guide follows the same depth as a full interview walkthrough: requirements → capacity → architecture → database → APIs → deep dives on generation and redirect → scalability, availability, edge cases, and security.
Design prompt
Design a URL shortening service that maps long URLs to short aliases and redirects users when they open the short link.
Support optional custom aliases, link expiration, and click analytics if time allows.
What you should be able to do after reading:
- State functional and non-functional requirements without skipping scale assumptions.
- Compute writes/sec, redirects/sec, yearly storage, bandwidth, and cache size with shown arithmetic.
- Draw load balancer, app servers, URL generator, redirect service, DB, cache, and optional analytics.
- Compare hash + Base62 vs incremental ID + Base62 with collision handling.
- Explain redirect caching, sharding tradeoffs, and when to use 301 vs 302.
1. Requirements gathering
Before drawing boxes, outline what the system must do and how well it must do it. Interviewers expect you to ask clarifying questions; below is a solid default scope.
1.1 Functional requirements
- Generate a unique short URL for a given long URL.
- Redirect the user to the original URL when the short URL is accessed.
- Custom short URLs (optional): user chooses
go.sharp/my-brandinstead of a random code. - Link expiration (optional): URL stops working after a date or TTL.
- Analytics (optional): click counts, referrer, geography, device type.
Usually out of scope unless asked: user accounts, link editing after creation, QR codes, A/B landing pages, browser extensions.
1.2 Non-functional requirements
- High availability — service up 99.9%+; redirect failures are immediately visible.
- Low latency — shortening and redirects in milliseconds; redirect is the hot path.
- Scalability — handle millions of requests per day with headroom for viral links.
- Durability — shortened URLs should keep working for years once created.
- Security — reduce phishing, spam, and abuse; HTTPS everywhere.
Assumptions used in the math below: 1 million new short URLs per day; read:write ratio 100:1; peak traffic 10× average; average original URL length 100 characters; 7-character Base62 short codes.
2. Capacity estimation
Show your work. Rounded numbers are fine; the interviewer wants to see that you know which dimensions to estimate.
2.1 Throughput requirements
Average writes per second (WPS):
1,000,000 requests / 86,400 seconds ≈ 12 WPS (average) Peak WPS ≈ 12 × 10 = 120 WPS
Read:write ratio 100:1 — for every URL created, expect ~100 redirects.
Average redirects/sec (RPS) ≈ 12 × 100 = 1,200 RPS Peak RPS ≈ 120 × 100 = 12,000 RPS
Redirect traffic dominates. Size caches, connection pools, and read replicas for 12k RPS peak, not for 120 WPS writes.
2.2 Storage estimation
Per shortened URL, store at minimum:
| Field | Size (approx.) |
|---|---|
| Short code (Base62) | 7 characters |
| Original URL | 100 characters (average) |
| Creation timestamp | 8 bytes |
| Expiration timestamp | 8 bytes |
| Click count (denormalized) | 4 bytes |
Storage per URL ≈ 7 + 100 + 8 + 8 + 4 = 127 bytes (metadata only; indexes add overhead) URLs per year = 1,000,000 × 365 = 365,000,000 Storage per year ≈ 365,000,000 × 127 bytes ≈ 46.4 GB
With indexes, replication, and analytics tables, plan for hundreds of GB over several years—still modest compared to video or log systems.
2.3 Bandwidth estimation
Assume each redirect response is ~500 bytes (status line, headers, small body if any).
Redirects per day ≈ 1,000,000 × 100 = 100,000,000 Read bandwidth ≈ 100,000,000 × 500 bytes ≈ 50 GB/day Peak: 500 bytes × 12,000 RPS ≈ 6 MB/s egress
Write bandwidth for POST /shorten is negligible next to redirects.
2.4 Caching estimation
Read-heavy systems benefit enormously from cache. Use the 80-20 rule: ~20% of URLs drive ~80% of clicks.
Daily writes = 1M → cache top 20% of that day's hot set Cache entries ≈ 1,000,000 × 0.2 × 127 bytes ≈ 25.4 MB
With a 90% cache hit ratio, only ~10% of redirects hit the database:
DB redirect load ≈ 1,200 × 0.10 ≈ 120 RPS (average)
That is comfortable for a sharded SQL or NoSQL store with read replicas. Without cache, every redirect hits disk—unacceptable at 12k RPS peak.
2.5 Infrastructure sizing (starting point)
| Layer | Sizing (initial) | Rationale |
|---|---|---|
| API / redirect servers | 4–6 instances behind LB, ~200–300 RPS each | Stateless; scale horizontally |
| Database | Distributed store, 10–20 nodes or managed equivalent | Billions of key lookups over lifetime |
| Cache | Redis cluster, 3–4 nodes | Sub-ms lookups; replicate for HA |
| Analytics queue | Kafka / SQS if analytics in scope | Decouple clicks from redirect latency |
3. High-level design
At a high level, the system needs these components:
- Load balancer — distributes traffic across app instances; terminates TLS.
- Application servers — handle shorten requests and redirect requests (can be same binary, different routes).
- URL generation service — creates short codes, validates custom aliases, applies expiration rules.
- Redirection service — resolves short code → long URL; returns HTTP redirect.
- Database — durable mapping store.
- Cache — hot mappings for fast redirects.
- Analytics service (optional) — click counts, geo, referrers.
flowchart TB
subgraph clients [Clients]
U[Browser / mobile / API]
end
subgraph edge [Edge]
LB[Load balancer]
RL[Rate limiter]
end
subgraph app [Application tier]
APP[App servers]
GEN[URL generation]
REDIR[Redirection]
end
subgraph data [Data tier]
CACHE[(Redis cache)]
DB[(Database)]
MQ[Message queue]
WH[(Analytics warehouse)]
end
U --> LB --> RL --> APP
APP --> GEN --> DB
APP --> REDIR
REDIR --> CACHE
REDIR --> DB
REDIR --> MQ --> WH
CACHE -.-> DB
Request flows (two paths)
Shorten (write path): validate URL → generate or validate code → insert row → return short URL. Latency budget: tens of ms; not as critical as redirect.
Redirect (read path): extract code → cache lookup → DB on miss → HTTP 301/302 → async analytics. Latency budget: single-digit ms on cache hit.
sequenceDiagram
participant C as Client
participant A as App server
participant G as URL generator
participant D as Database
C->>A: POST /shorten long_url
A->>G: assign short_code
G-->>A: abc12XY
A->>D: INSERT mapping
D-->>A: OK
A-->>C: 201 short_url
4. Database design
4.1 SQL vs NoSQL
Both are defensible in interviews. Choose based on access pattern and ops comfort.
| Factor | Favors SQL (PostgreSQL) | Favors NoSQL (DynamoDB / Cassandra) |
|---|---|---|
| Access pattern | Point lookup by short_code; UNIQUE constraints | Billions of simple key-value gets |
| Custom alias + transactions | Natural fit | Conditional writes; design carefully |
| Scale-out | Read replicas + sharding by key | Built-in partitioning |
| Analytics joins | Easier with SQL warehouse ETL | Stream to warehouse separately |
Interview default: SQL primary + read replicas + Redis cache. Mention DynamoDB if the interviewer wants hyperscale with minimal ops.
4.2 Database schema
Two logical tables: URL mappings and users (if accounts exist). For anonymous shorten-only scope, the users table is optional but good to mention.
URL mappings
CREATE TABLE url_mappings ( id BIGSERIAL PRIMARY KEY, short_code VARCHAR(16) NOT NULL, long_url TEXT NOT NULL, long_url_hash CHAR(64), -- SHA-256 for dedup lookups user_id BIGINT REFERENCES users(id), is_custom_alias BOOLEAN DEFAULT FALSE, expires_at TIMESTAMPTZ, click_count BIGINT DEFAULT 0, -- optional denormalized counter created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_active BOOLEAN NOT NULL DEFAULT TRUE, UNIQUE (short_code) ); CREATE INDEX idx_url_long_hash ON url_mappings (long_url_hash) WHERE is_active = TRUE;
Users (optional)
CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );
Click events (analytics)
CREATE TABLE click_events ( id BIGSERIAL PRIMARY KEY, short_code VARCHAR(16) NOT NULL, clicked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), referrer TEXT, country CHAR(2), user_agent TEXT );
At scale, raw click events often land in Kafka and aggregate in ClickHouse or BigQuery instead of growing unbounded in OLTP.
5. API design
REST-style endpoints that are easy to explain on a whiteboard.
5.1 URL shortening API
POST /shorten
Request body:
{
"long_url": "https://www.example.com/some/very/long/url",
"custom_alias": "my-link", // optional
"expires_at": "2027-12-31T23:59:59Z" // optional
}
Response 201 Created:
{
"short_url": "https://go.sharp/my-link",
"short_code": "my-link",
"long_url": "https://www.example.com/some/very/long/url",
"expires_at": "2027-12-31T23:59:59Z"
}
Errors: 400 invalid URL · 409 alias taken · 429 rate limited.
5.2 URL redirection API
GET /{short_url_key}
Response (301 example — permanent redirect):
HTTP/1.1 301 Moved Permanently Location: https://www.example.com/some/very/long/url
Use 302 Found when you need accurate per-click analytics or may change the destination later. Use 301 when the mapping is permanent and you want browsers/CDNs to cache the redirect (fewer origin hits, under-counted clicks).
404 Not Found — unknown code · 410 Gone — expired link.
5.3 Analytics API (optional)
GET /api/v1/urls/{short_code}/stats
{
"short_code": "abc12XY",
"total_clicks": 48291,
"clicks_by_day": [ ... ]
}
6. Diving deep into key components
6.1 URL generator service
The generator must produce a short, unique, URL-safe key. Compare approaches on length, scalability, and collisions.
Approach 1: Hashing and encoding
Hash the long URL (MD5 or SHA-256), take a portion of the digest, convert to decimal, then Base62-encode. Base62 uses A–Z, a–z, 0–9 (62 characters)—URL-friendly and dense.
A 7-character Base62 string supports 627 ≈ 3.5 trillion unique codes.
Example workflow (illustrative):
- Long URL:
https://www.example.com/some/very/long/url/that/needs/to/be/shortened - MD5 hash (hex):
1b3aabf5266b0f178f52e45f4bb430eb - Take first 6 bytes:
1b3aabf5266b→ decimal47770830013755 - Base62 encode → short code e.g.
DZFbb43(length ~7 depends on value)
Issues with hash-only:
- Same long URL always yields same short URL (may or may not be desired).
- Collisions: two different long URLs can produce the same truncated hash (rare but possible).
Collision resolution strategies:
- Re-hash with a salt or use additional hash bytes until unique.
- Incremental suffix — append
-1,-2to the short code until the DB insert succeeds.
Approach 2: Unique ID generation
Assign each URL a monotonic numeric ID (1, 2, 3, …), then Base62-encode the ID. No collisions if the ID source is authoritative.
Considerations:
- Predictability — sequential codes leak volume; attackers can scan neighbors. Mitigate with obfuscation, random offset, or Snowflake IDs before encoding.
- Scalability — a single DB sequence is a hot row. Mitigate with Key Generation Service (KGS) pre-allocating batches, or distributed IDs (Twitter Snowflake).
What to say in the interview: “I’d use Base62 of a distributed unique ID for default links—collision-free and fast. I’d use hash+retry only if product requires deterministic shortening. Custom aliases are a separate namespace with a UNIQUE constraint.”
Custom aliasing
- Uniqueness check — lookup before insert; return 409 if taken.
- Character validation — alphanumeric and hyphen only; reject Unicode homoglyphs if abuse is a concern.
- Reserved words — block
admin,api,help,login, etc. - Conflict resolution — suggest alternatives on 409 (
my-link-2).
Link expiration
- User-specified — validate future date and max TTL (e.g. 5 years).
- Default expiration — e.g. 1 year if not specified, or never (product choice).
- Background job — cron marks expired rows inactive and purges cache keys.
- Real-time check — redirect path compares
now()toexpires_at; return 410 if expired.
6.2 Redirection service
When a user opens a short link, the service must resolve the code and issue an HTTP redirect—fast.
Example workflow:
- User opens
https://go.sharp/abc123. - Service extracts
abc123. - Check Redis for
url:abc123→ if hit, read long URL. - On miss, query database (read replica), then
SETcache with TTL. - Verify not expired; return 301/302 with
Location: long_url. - Publish click event to queue (non-blocking).
sequenceDiagram
participant U as User browser
participant R as Redirection service
participant C as Redis
participant D as Database
participant Q as Analytics queue
U->>R: GET /abc123
R->>C: GET url:abc123
alt cache hit
C-->>R: long_url
else cache miss
R->>D: SELECT long_url WHERE short_code
D-->>R: row
R->>C: SET url:abc123 TTL 24h
end
R->>Q: click event async
R-->>U: 302 Location long_url
Caching for performance
Redis (or Memcached) in front of the database cuts p99 latency and protects the DB during viral links.
- Cache-aside — app loads on miss; no write-through required for redirects.
- TTL — 24h default; shorter for low-traffic codes if memory is tight.
- Stampede protection — single-flight lock per key on miss so 10k concurrent requests don’t all hit DB.
- Invalidation — on URL update/delete/expiry, delete cache key explicitly.
6.3 Analytics service (optional)
Never block redirect on analytics.
- Event logging — each redirect emits a message to Kafka/SQS with
short_code, timestamp, referrer, IP (hashed), user-agent, country. - Stream processing — Flink/Spark or consumer workers update real-time counters in Redis.
- Batch processing — nightly loads into a warehouse for dashboards and geo breakdowns.
flowchart LR
REDIR[Redirect service] --> Q[Kafka]
Q --> RT[Real-time counters]
Q --> BATCH[Batch ETL]
BATCH --> WH[(Data warehouse)]
7. Addressing key issues and bottlenecks
7.1 Scalability
API layer
Run stateless app servers behind a load balancer. Scale out when CPU or connection count rises. Separate redirect-only pools from shorten API if shorten traffic spikes during campaigns (optional optimization).
Database sharding
When one node is not enough, shard by short_code.
| Strategy | How | Pros | Cons |
|---|---|---|---|
| Range-based | IDs 1–1M on shard A, 1M+1–2M on shard B | Simple with auto-increment IDs | Hot last shard; uneven growth |
| Hash-based | hash(short_code) % N |
Even spread | Resharding painful without consistent hashing |
Mention consistent hashing when adding shards without massive data movement.
Caching
Redis cluster with replication; cache the mapping, not the full HTTP response (destinations differ in headers). Monitor hit ratio—if it drops below ~85%, add memory or tune TTL.
7.2 Availability
- Database replication — primary for writes, replicas for redirect misses; automatic failover.
- Cache HA — Redis Sentinel or cluster mode; redirect still works on cache miss if DB is up.
- Failover — health checks on app servers; LB removes bad instances.
- Geo-distributed deployment (stretch goal) — read replicas or multi-region cache in EU/US/Asia to cut redirect latency; conflict resolution only matters if you allow multi-master writes (usually avoid).
- Graceful degradation — if analytics queue is down, still redirect; if cache is down, fall back to DB (slower but correct).
7.3 Handling edge cases
| Scenario | Behavior |
|---|---|
| Expired URL | HTTP 410 Gone with friendly page; do not redirect |
| Non-existent code | HTTP 404 Not Found |
| Hash or alias collision | Detect on create; retry generation or reject with 409 |
| Duplicate long URL | Return existing short URL (dedup) or create new—state product choice |
| Malicious long URL | Block at shorten time via Safe Browsing / internal denylist |
| Redirect loops | Reject shortening a URL that points to your own domain (optional) |
7.4 Security
- Rate limiting — per IP and per API key on
POST /shortenand optionally on redirect to mitigate abuse. - Input validation — only
http/httpsschemes; max URL length; block private IPs if policy requires (SSRF prevention for server-side fetch features). - HTTPS — TLS on all client-facing endpoints.
- Monitoring and alerts — spike in 404 rate, shorten rate from single ASN, redirect latency SLO burn.
- Phishing — report-abuse flow; take down malicious codes; share denylist across regions.
8. Tradeoffs recap (whiteboard close)
| Topic | Common choice | One-line why |
|---|---|---|
| Short code algorithm | Base62(unique ID) | No collisions; predictable length |
| Primary store | SQL or DynamoDB | Point lookups by short_code at huge scale |
| Redirect code | 302 for analytics; 301 for permanence | Product depends on measurement vs cache |
| Cache | Redis cache-aside | 90%+ hit ratio → DB sees ~120 RPS not 1,200 |
| Analytics | Async queue | Redirect path stays milliseconds |
9. How to present this in 35–45 minutes
- 5 min — requirements + assumptions (1M writes/day, 100:1 read ratio).
- 8 min — capacity: WPS, RPS, 46 GB/year, cache 25 MB hot set, 120 RPS to DB with 90% cache hit.
- 7 min — diagram: LB, app, DB, Redis, optional Kafka.
- 10 min — APIs + schema; Base62 ID vs hash; custom alias + expiration.
- 8 min — redirect + cache sequence; 301 vs 302; sharding and security if asked.
- 5 min — tradeoffs and future work (multi-region, custom domains).
The one line to remember
A URL shortener is a read-optimized key-value lookup: generate a unique short code once,
cache short_code → long_url aggressively, redirect in one HTTP hop, and record analytics off the critical path.