URL Shortener System Design - 1 Hour Interview Guide
Design a scalable URL Shortener like Bitly or TinyURL. Covers requirements, capacity estimation, API design, URL encoding, redirect flow, caching, database design, sharding, analytics, and trade-offs for a 1-hour system design interview.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 15 min | API design |
| 15 – 25 min | High-level architecture |
| 25 – 35 min | URL encoding + database design |
| 35 – 45 min | Redirect flow + caching |
| 45 – 55 min | Scaling + analytics + rate limiting |
| 55 – 60 min | Trade-offs + failure scenarios |
What Is a URL Shortener?
A URL Shortener takes a long URL and produces a short, unique alias that redirects users to the original URL.
Example:
Input: https://www.example.com/products/shoes/running/men/size-12?color=blue&promo=SAVE20
Output: https://short.ly/xK3mP9
When a user visits https://short.ly/xK3mP9, the system looks up the original URL and redirects the browser in milliseconds.
Real-world examples: Bitly, TinyURL, t.co (Twitter), goo.gl (Google)
Why Is This a Good Interview Problem?
A URL Shortener is deceptively simple but covers the full breadth of system design:
- Read-heavy workload with extreme redirect throughput
- Unique ID generation at scale
- Caching strategy
- Database choice and sharding
- Async analytics pipeline
- Rate limiting and abuse prevention
Step 1 — Requirements
Functional Requirements
| # | Requirement |
|---|---|
| 1 | Given a long URL, generate a short URL (6–8 characters) |
| 2 | Redirect a short URL to the original long URL |
| 3 | Support custom aliases (e.g. short.ly/my-promo) |
| 4 | Support URL expiration (TTL) |
| 5 | Track click analytics (count, device, country, referrer) |
| 6 | Allow users to delete or disable their short URLs |
Non-Functional Requirements
| # | Requirement |
|---|---|
| 1 | Redirect latency < 100ms (p99) |
| 2 | High availability — 99.99% uptime |
| 3 | Short codes must be globally unique |
| 4 | Eventual consistency acceptable for analytics |
| 5 | System must handle 100:1 read-to-write ratio |
| 6 | No predictable or guessable short codes |
Out of Scope
- User authentication UI
- Full analytics dashboard UI
- Billing or subscription management
Step 2 — Capacity Estimation
Assumptions
| Metric | Value |
|---|---|
| New URLs created per day | 100 million |
| URL read/redirect ratio | 100:1 |
| Average URL size | 500 bytes |
| Short code size | ~50 bytes |
| Analytics record size | ~200 bytes |
| Data retention | 5 years |
Write Volume
100 million URLs/day
= ~1,160 writes/second
Read Volume
100 million × 100 = 10 billion redirects/day
= ~115,000 reads/second (peak ~300,000/sec)
Storage (5 years)
URLs: 100M/day × 365 × 5 × 550 bytes ≈ 100 TB
Analytics: 10B/day × 365 × 5 × 200 bytes ≈ 3.6 PB
Key Insight
This is an extremely read-heavy system. The redirect path must be optimized above all else. Caching is not optional — it is the core design decision.
Step 3 — API Design
Create Short URL
POST /api/v1/urls
Request:
{
"long_url": "https://example.com/very-long-path",
"custom_alias": "my-promo", // optional
"expires_at": "2025-12-31T00:00:00Z" // optional
}
Response 201:
{
"short_url": "https://short.ly/xK3mP9",
"short_code": "xK3mP9",
"expires_at": "2025-12-31T00:00:00Z"
}
Redirect
GET /{short_code}
Response 302: Location: https://example.com/very-long-path
Get Analytics
GET /api/v1/urls/{short_code}/analytics
Response 200:
{
"short_code": "xK3mP9",
"total_clicks": 48291,
"unique_visitors": 31042,
"top_countries": ["US", "IN", "GB"],
"top_devices": ["mobile", "desktop"]
}
Delete URL
DELETE /api/v1/urls/{short_code}
Response 204: No Content
Step 4 — High-Level Architecture
flowchart TD
Client --> AG[API Gateway]
AG --> US[URL Service]
AG --> RS[Redirect Service]
US --> DB[(Primary DB)]
RS --> RC[(Redis Cache)]
RC -->|cache miss| DB
RS --> AQ[Analytics Queue\nKafka]
AQ --> AS[Analytics Service]
AS --> AD[(Analytics Store)]
US --> IDG[ID Generator\nSnowflake]
Component Responsibilities
| Component | Responsibility |
|---|---|
| API Gateway | Rate limiting, authentication, routing |
| URL Service | Create, validate, and store short URL mappings |
| Redirect Service | Look up short code → long URL, issue HTTP redirect |
| Redis Cache | Cache hot short code → long URL mappings for fast lookups |
| ID Generator | Generate globally unique, non-sequential IDs (Snowflake) |
| Analytics Queue | Receive click events asynchronously (Kafka) |
| Analytics Service | Process click events, write to analytics store |
| Analytics Store | Time-series and aggregated click data (S3 + Redshift) |
Step 5 — URL Encoding
The Core Problem
How do you generate a short, unique, non-guessable code for every URL?
Option Comparison
| Approach | Pros | Cons |
|---|---|---|
| Auto-increment ID | Simple, no collisions | Predictable, enumerable |
| UUID | Unique, no coordination needed | Too long (32 chars) |
| MD5/SHA256 hash | Deterministic | Collisions possible, truncation required |
| Random string | Unpredictable | Collision probability grows with scale |
| Base62 + Snowflake | Unique, compact, fast | Requires distributed ID service |
Recommended Approach — Base62 Encoding
Step 1: Generate a unique 64-bit integer using a Snowflake ID generator.
Step 2: Encode that integer to Base62.
Base62 character set:
0-9 → 10 characters
a-z → 26 characters
A-Z → 26 characters
Total: 62 characters
Why Base62?
- No special characters — URL-safe
- 6 characters = 62⁶ = 56 billion unique codes
- 7 characters = 62⁷ = 3.5 trillion unique codes
Snowflake ID
A Snowflake ID is a 64-bit number composed of:
| 41 bits timestamp | 10 bits machine ID | 12 bits sequence |
- Monotonically increasing — no coordination between nodes needed
- K-sortable — newer IDs are always larger
- ~4 million IDs/second per node
Encoding Flow
flowchart LR
LU[Long URL] --> US[URL Service]
US --> SN[Snowflake\nID Generator]
SN -->|64-bit ID| B62[Base62 Encoder]
B62 -->|short_code| DB[(Database)]
B62 -->|short_code| Resp[Return short URL]
Collision Handling
With Snowflake IDs, collisions cannot occur — each ID is guaranteed unique.
For custom aliases, check uniqueness before saving:
- If alias already exists → return
409 Conflict - If alias is available → save and return
201 Created
Step 6 — Database Design
URL Mapping Table
Table: url_mappings
short_code VARCHAR(8) PRIMARY KEY
long_url TEXT NOT NULL
user_id BIGINT nullable (if authenticated)
created_at TIMESTAMP NOT NULL
expires_at TIMESTAMP nullable
is_active BOOLEAN DEFAULT true
Index: short_code — the only column queried on every redirect.
Analytics Table
Table: click_events
id BIGINT PRIMARY KEY
short_code VARCHAR(8) NOT NULL
clicked_at TIMESTAMP NOT NULL
country VARCHAR(2)
device_type VARCHAR(20)
referrer TEXT
ip_hash VARCHAR(64) (hashed for privacy)
Database Choice
| Factor | Recommendation |
|---|---|
| Read pattern | Point lookup by short_code — very fast |
| Write pattern | Insert once, rarely update |
| Scale requirement | Billions of rows (sharding needed) |
| Consistency | Strong for URL creation, eventual for analytics |
| Recommendation | PostgreSQL (primary) or DynamoDB (fully managed, auto-sharding) |
Step 7 — Redirect Flow
The Critical Path
Every millisecond matters here. The redirect path must be as fast as possible.
flowchart TD
User -->|GET /xK3mP9| RS[Redirect Service]
RS -->|check| LC[Local Cache\nCaffeine]
LC -->|hit| Redir[302 Redirect]
LC -->|miss| RC[Redis Cache]
RC -->|hit| Redir
RC -->|miss| DB[(Database)]
DB --> RC
DB --> Redir
Redir --> AQ[Kafka\nClick Event]
Cache Layers
| Layer | Technology | TTL | Capacity | Purpose |
|---|---|---|---|---|
| Local cache | Caffeine | 60 sec | ~10K entries/pod | Fastest — avoid Redis for hot URLs |
| Distributed | Redis | 24 hrs | Billions | Shared across all redirect pods |
| Origin | PostgreSQL / DynamoDB | — | All data | Source of truth |
301 vs 302 Redirect
| Type | Meaning | Browser Behavior | When to Use |
|---|---|---|---|
| 301 | Permanent | Browser caches it — no future requests | Reduces server load; loses analytics |
| 302 | Temporary | Browser always requests server | Preserves analytics; more load |
Recommendation: Use 302 to preserve click analytics. Use 301 only for permanent redirects where analytics are not needed.
Cache Key
Key: short_code → "xK3mP9"
Value: long_url + expires_at → "https://example.com/... | 2025-12-31"
Step 8 — Analytics Design
Problem
Analytics must not slow down redirects. Writing to a database synchronously on every redirect would add 20–50ms of latency.
Solution — Async Analytics Pipeline
flowchart LR
RS[Redirect Service] -->|publish event| KF[Kafka Topic\nclick-events]
KF --> AC[Analytics Consumer]
AC --> AGG[Aggregation Service]
AGG --> TS[(Time-Series DB\nRedshift / ClickHouse)]
AGG --> RC2[Redis counters\nfor real-time counts]
Click Event Schema
{
"short_code": "xK3mP9",
"timestamp": "2025-06-15T10:23:45Z",
"ip_hash": "sha256(...)",
"country": "US",
"device": "mobile",
"referrer": "https://twitter.com"
}
Click Count Accuracy
| Approach | Accuracy | Complexity | Use Case |
|---|---|---|---|
| Exact DB count | 100% | High | Billing-critical |
| Redis counter | ~99% | Medium | Real-time dashboards |
| HyperLogLog | ~98% | Low | Unique visitor counts |
Recommendation: Redis counters for real-time display; batch reconcile with Kafka-sourced counts nightly.
Step 9 — Scaling
The Challenge
115,000 redirects/second sustained — peaks to 300,000/second.
Read Scaling
flowchart TD
LB[Load Balancer] --> R1[Redirect Pod]
LB --> R2[Redirect Pod]
LB --> R3[Redirect Pod]
R1 --> Redis[(Redis Cluster)]
R2 --> Redis
R3 --> Redis
Redis -->|miss| DBR[DB Read Replicas]
- Redirect Service is stateless — scale horizontally to any number of pods
- Redis Cluster handles billions of cached entries with sub-millisecond reads
- Read replicas for DB — redirect service reads from replicas, not primary
Database Sharding
At billions of rows, a single database cannot handle the load.
Shard key: short_code (consistent hash)
flowchart TD
RS[Redirect Service] --> SH[Shard Router]
SH -->|hash xK3mP9| S1[Shard 1\nA-F]
SH -->|hash mN7qR2| S2[Shard 2\nG-M]
SH -->|hash tY9pW5| S3[Shard 3\nN-Z]
- Consistent hashing avoids reshuffling all data when adding shards
- Short code naturally distributes evenly across shards
CDN for Popular URLs
For viral URLs (millions of hits per hour), push the redirect response to CDN edge nodes. The redirect happens at the CDN — zero load on origin.
Step 10 — Rate Limiting and Security
Rate Limiting
Applied at the API Gateway layer:
| Operation | Limit | Reason |
|---|---|---|
| Create URL | 100 requests/min per user | Prevent spam / abuse |
| Redirect | 1,000 requests/min per IP | Prevent scraping / enumeration |
| Analytics | 60 requests/min per user | Protect analytics backend |
Algorithm: Token Bucket — handles burst traffic naturally.
Security Concerns
| Threat | Defense |
|---|---|
| Malicious URL shortened | Validate against Google Safe Browsing API on create |
| Enumeration of short codes | Non-sequential Snowflake-based codes |
| Redirect bombing (DoS) | Rate limit by IP at gateway |
| Phishing via short URLs | Block known phishing domains, allow admin takedown |
| Private URL exposure | Optional password-protected short URLs |
Step 11 — Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Redis goes down | All redirects hit DB directly | Local in-process cache absorbs short burst; DB read replicas handle sustained load |
| DB goes down | Cache still serves cached URLs | Read replicas + circuit breaker; write queue for new URLs |
| Kafka goes down | Analytics events lost | Redirect still works; use local buffer + retry; eventual consistency acceptable |
| ID Generator down | Cannot create new URLs | Multiple Snowflake nodes; fall back to secondary node |
| Single region down | Full outage for that region | Multi-region active-active; DNS failover |
Final Architecture
flowchart TD
Client --> CDN[CDN\nHot URL Redirects]
Client --> AG[API Gateway\nRate Limiter + Auth]
AG --> US[URL Service]
AG --> RS[Redirect Service\nStateless × N pods]
US --> IDG[Snowflake\nID Generator]
US --> DB_P[(Primary DB\nPostgreSQL)]
RS --> LC[Local Cache\nCaffeine]
RS --> RC[(Redis Cluster)]
RC -->|miss| DB_R[(DB Read Replicas)]
DB_R --> DB_P
RS -->|async| KF[Kafka\nclick-events]
KF --> AC[Analytics Consumer]
AC --> ADS[(Analytics Store\nRedshift + S3)]
Technology Stack
| Layer | Technology |
|---|---|
| API Gateway | AWS API Gateway / Kong / NGINX |
| URL Service | Spring Boot / Node.js |
| Redirect Service | Spring Boot / Go (latency-optimized) |
| ID Generator | Twitter Snowflake / Boundary Flake |
| Cache (local) | Caffeine |
| Cache (distributed) | Redis Cluster |
| Primary Database | PostgreSQL / DynamoDB |
| Analytics Queue | Apache Kafka / AWS Kinesis |
| Analytics Store | Redshift + S3 / ClickHouse |
| CDN | CloudFront / Cloudflare |
| Monitoring | Prometheus + Grafana / CloudWatch |
| Deployment | Kubernetes / AWS ECS |
Key Trade-Offs
| Decision | Option A | Option B | Choice & Reason |
|---|---|---|---|
| Redirect type | 301 Permanent | 302 Temporary | 302 — preserves analytics; browser won't cache |
| Short code generation | UUID | Base62 + Snowflake | Base62 — compact, URL-safe, no collisions |
| Analytics write | Synchronous | Async via Kafka | Async — never slow down the redirect critical path |
| Database | SQL (PostgreSQL) | NoSQL (DynamoDB) | Either works; DynamoDB if fully managed scale needed |
| Caching | Redis only | Redis + local cache | Both — local cache absorbs hot URL traffic |
| Consistency | Strong everywhere | Eventual for analytics | Eventual for analytics; strong for URL creation |
Common Interview Mistakes
- ❌ Starting with the database schema before gathering requirements
- ❌ Forgetting that this is a 100:1 read-heavy system — design for reads first
- ❌ No caching layer — the DB cannot handle 115K redirects/second alone
- ❌ Writing analytics synchronously on the redirect path
- ❌ Using sequential IDs — predictable and enumerable
- ❌ No collision strategy for custom aliases
- ❌ No discussion of 301 vs 302 and the analytics trade-off
- ❌ No rate limiting or abuse prevention
- ❌ No failure scenario discussion
Interview Questions
- How do you generate unique short codes at scale?
- Why is Base62 preferred over UUID or MD5?
- Why is this system read-heavy and how does that affect design?
- Walk me through a redirect from browser to destination.
- What is the difference between a 301 and 302 redirect?
- What happens when Redis goes down?
- How do you handle hot URLs that receive millions of hits?
- Why should analytics be written asynchronously?
- How would you shard the URL database?
- How do you prevent malicious or phishing URLs?
- How do you support URL expiration?
- How do you support custom aliases and handle conflicts?
- How would you scale the redirect service to 300K req/sec?
- How does consistent hashing help when adding shards?
- What are the trade-offs between exact click counts and approximate counts?
Summary
A URL Shortener is a classic system design problem that tests your ability to reason about:
| Concern | Solution |
|---|---|
| Unique short codes | Snowflake ID → Base62 encoding |
| Fast redirects | Stateless service + Redis cache + local Caffeine cache |
| Redirect semantics | 302 (preserves analytics) |
| Analytics | Async Kafka pipeline — never on the critical path |
| Scale | Horizontal pod scaling + DB read replicas + sharding |
| Abuse prevention | Rate limiting + URL validation + admin takedown |
| High availability | Multi-region, Redis cluster, DB replicas, CDN |
The core principle: Keep the redirect path fast, cached, stateless, and always available. Everything else is secondary.