Requirements & scope
Problem Statement & Requirements
Functional Requirements
- Given a long URL, generate a short, unique alias (e.g.,
https://short.ly/abc123) - When a user hits the short URL, redirect them to the original long URL (HTTP 301/302)
- Users can optionally pick a custom short alias
- Links can have an expiration time (TTL)
- Analytics: track click count, referrer, geo, device (optional but expected in interviews)
Non-Functional Requirements
- High availability — redirections must never go down
- Low latency — redirect in < 10 ms (p99)
- URL should not be guessable — no sequential IDs
- Consistency — same long URL can map to multiple short URLs (user-specific), but a short URL must always resolve to exactly one long URL
- Durability — once created, a short link must survive infrastructure failures
Out of Scope
- User authentication/accounts (simplify for interview)
- Link editing after creation
- Bulk URL shortening API
Scale estimations
Scale Estimations
Traffic
| Metric | Value |
|---|---|
| New URLs created / day | 100M (write-heavy assumption) |
| Reads (redirections) / day | 10B (read:write = 100:1) |
| Reads / second | ~116K RPS |
| Writes / second | ~1,160 RPS |
| Peak reads / second | ~350K RPS (3x average) |
Storage
| Metric | Value |
|---|---|
| Average URL length | 500 bytes |
| Metadata per URL | 100 bytes (created_at, expires_at, user_id, click_count) |
| Short key | 7 bytes |
| Total per record | ~607 bytes → round to 1 KB |
| Records over 5 years | 100M/day × 365 × 5 = 182.5B records |
| Storage over 5 years | 182.5B × 1 KB = ~182.5 TB |
Bandwidth
| Metric | Value |
|---|---|
| Incoming (writes) | 1,160 × 1 KB = ~1.16 MB/s |
| Outgoing (reads) | 116K × 1 KB = ~116 MB/s |
Cache
- 80/20 rule: 20% of URLs generate 80% of traffic
- Cache 20% of daily read requests: 10B × 0.2 × 1 KB = ~2 TB cache
- Fits in a Redis cluster
Layered architecture
Phase 1 of 4 · Client & edge
Start with the entry points
First, define who uses the system and where requests enter. This is the boundary of the design, not yet a complete request path.
- Client
- Start with the two user actions: create a short link and follow an existing one.
- API gateway
- Separate creation from redirection, then protect both endpoints from abusive traffic.
This first layer defines the system boundary. The next chapters add application logic and data dependencies. After examining the request paths and tradeoffs, we will add operational safeguards and bring the complete architecture together.
API & contracts
Phase 2 of 4 · Application & compute
Add the application logic
Keep the entry points and add the services that do the work. Give each service a clear responsibility before defining its contracts. Storage and background dependencies come next.
- URL service
- Validate URLs, enforce expiry, and resolve aliases. Stateless servers can scale horizontally.
- Key generator
- Generate non-sequential keys and use a conditional insert to detect collisions, retrying when necessary.
API Design
Create Short URL
POST /api/v1/urls
Content-Type: application/json
Request:
{
"long_url": "https://www.example.com/very/long/path?q=something",
"custom_alias": "my-brand", // optional
"expires_at": "2026-12-31T00:00:00Z" // optional
}
Response (201 Created):
{
"short_url": "https://short.ly/abc123",
"short_key": "abc123",
"long_url": "https://www.example.com/very/long/path?q=something",
"expires_at": "2026-12-31T00:00:00Z",
"created_at": "2026-04-03T10:00:00Z"
}Redirect (Read)
GET /:short_key
→ HTTP 301 (permanent) or 302 (temporary) redirect to long_url
301 vs 302 Tradeoff:
- 301: Browser caches the redirect → less load on our servers,
but we lose analytics visibility (browser skips us on repeat visits)
- 302: Browser always hits us → more load but full analytics tracking
→ Use 302 if analytics matter; 301 if pure performance is the goalDelete URL
DELETE /api/v1/urls/:short_key
→ 204 No ContentData model
Phase 3 of 4 · Data & async work
Connect data and background work
The services now need their dependencies: durable state, fast lookups, or work that continues outside a request. Separate these responsibilities rather than treating every component as a database.
- URL database
- Persist each alias-to-URL mapping durably. Point lookups make a key-value access pattern a natural fit.
- Redis cache
- Cache popular redirects to avoid a database lookup on every request. Never serve links beyond their expiry.
- Click stream
- Publish click events outside the latency-critical redirect path, with bounded buffering on failure.
Data Model
URL Table (Primary)
Database Choice
| Option | Pros | Cons |
|---|---|---|
| DynamoDB | Managed, auto-scaling, single-digit ms latency, partition by short_key | No joins (not needed here), cost at extreme scale |
| Cassandra | Linear scalability, tunable consistency, great for write-heavy | Operational complexity, eventual consistency |
| MySQL + sharding | ACID, familiar, strong consistency | Manual sharding, operational burden at scale |
Recommendation: DynamoDB or Cassandra — this is a key-value lookup workload with high write throughput. No joins, no complex queries.
Core design decisions
Key Generation — The Core Design Decision
This is the most important part of the design. How do we generate a short, unique, non-guessable key?
Option 1: Hash the Long URL (MD5/SHA-256 + Base62 Truncation)
Key space: Base62 with 7 characters = 62^7 = 3.5 trillion unique keys
Collision handling:
| Pros | Cons |
|---|---|
| Deterministic — same URL → same hash | Collisions require DB lookup + retry |
| Simple to implement | Same long URL → same short URL (no per-user links) |
| No coordination needed | MD5 is not cryptographically secure (fine for this use) |
Option 2: Pre-Generated Key Pool (Key Generation Service — KGS)
How it works:
- Offline job generates random 7-char Base62 keys, stores in
unused_keystable - App server requests a batch (e.g., 1,000 keys) at startup, caches them in memory
- On URL creation, pop a key from the in-memory batch
- When batch runs low (< 200), fetch another batch asynchronously
- Mark keys as "used" in the DB atomically
Concurrency safety:
- Use a two-table approach:
unused_keysandused_keys - Move key from unused → used in a single transaction
- If an app server crashes, its unused in-memory keys are "lost" — acceptable given the 3.5T key space
| Pros | Cons |
|---|---|
| Zero collision — guaranteed unique | Need a separate KGS service |
| O(1) key generation — no DB check needed | Pre-generated keys take storage |
| Fast — just pop from in-memory cache | Wasted keys if server crashes (negligible) |
| No hash computation | Single point of failure → need standby KGS |
Option 3: Counter-Based with Encoding (Snowflake-style)
| Pros | Cons |
|---|---|
| Zero collision — ranges don't overlap | Sequential/predictable (guessable) |
| No DB lookup for uniqueness check | Requires Zookeeper or similar coordination |
| Simple, efficient | Range exhaustion needs re-allocation |
Recommendation
Use Option 2 (KGS) for production systems:
- No collisions, no coordination overhead per request
- O(1) amortized key generation
- Easy to make non-guessable (random keys, not sequential)
- Can be made highly available with standby KGS replicas
Request flows
Detailed Flow Diagrams
Write Flow (URL Creation)
Read Flow (Redirection)
Analytics Flow (Async)
Performance & caching
Caching Strategy
Cache Architecture
Caching Patterns
| Pattern | How It Works | When to Use |
|---|---|---|
| Cache-Aside | App checks cache → miss → query DB → populate cache | ✅ Best for this use case |
| Write-Through | Write to cache + DB on creation | Wastes cache on URLs that may never be read |
| Write-Behind | Write to cache, async flush to DB | Risky — URL could be lost if cache crashes |
Recommendation: Cache-Aside with Write-on-Create for popular URLs
- On URL creation: don't populate cache (most URLs are rarely accessed)
- On first read: query DB, populate cache
- Exception: if we detect a URL going viral (high click rate), proactively cache it
Cache Warming
- Preload the top 1% most-accessed URLs into cache on service startup
- Use a daily batch job scanning analytics to identify hot URLs
Advanced design
Database Sharding & Partitioning
Sharding Strategy
Recommendation: Consistent Hashing on short_key
- Even distribution
- Adding/removing DB nodes only requires moving ~1/N of the keys
- Use virtual nodes (vnodes) for better balance — 150-200 vnodes per physical node
Replication
Edge cases
Handling Edge Cases
Duplicate Long URLs
- Same user, same URL: Return existing short URL (dedup query on
long_url + user_idindex) - Different users, same URL: Generate separate short URLs (user isolation)
Expired URLs
- Lazy deletion: On read, check
expires_at→ if expired, return 410 Gone - Background cleanup: Cron job deletes expired records, recycles keys back to KGS
Custom Aliases
- Validate: 3-16 chars, alphanumeric + hyphens only
- Check uniqueness in DB
- Reserve a set of blacklisted words (profanity, reserved paths like
/api,/admin)
Hot URLs (Viral Links)
- A single URL getting millions of hits/second
- Solution: Cache replication — replicate hot keys across multiple cache shards
- Application-level: detect hot keys (e.g., > 1000 RPS) and fan out reads across replicas
Tradeoffs
Tradeoffs & Design Decisions Summary
| Decision | Option A | Option B | Chosen | Why |
|---|---|---|---|---|
| Key Generation | Hash + collision check | Pre-generated KGS | KGS | Zero collisions, O(1), non-guessable |
| Redirect Code | 301 (permanent) | 302 (temporary) | 302 | Preserves analytics; 301 loses repeat visit tracking |
| Database | SQL (MySQL/Postgres) | NoSQL (DynamoDB/Cassandra) | NoSQL | Key-value access pattern, high write throughput, linear scaling |
| Caching | Write-through | Cache-aside | Cache-aside | Most URLs are rarely accessed; avoid wasting cache on cold URLs |
| Sharding | Range-based | Consistent hashing | Consistent hashing | Even distribution, graceful scaling |
| ID Length | 6 chars (56B keys) | 7 chars (3.5T keys) | 7 chars | Room for 5+ years at 100M URLs/day |
| Analytics | Synchronous in request path | Async via Kafka | Async Kafka | Don't add latency to the redirect hot path |
Reliability & fault tolerance
Phase 4 of 4 · Operations & resilience
Add operational safeguards
With the request paths and tradeoffs established, add the controls that help detect trouble and keep the system operating. The final diagram retains every layer introduced so far.
- Observability
- Track redirect latency, cache misses, collisions, and queue lag. Analytics failure must not stop redirection.
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
Graceful Degradation
- If cache is down → serve directly from DB (higher latency, still functional)
- If KGS is down → app servers use remaining in-memory keys; alert for manual intervention
- If analytics pipeline is down → buffer events locally, replay later
Production architecture
High-Level Architecture
Component Breakdown
Full System Architecture (Production-Grade)
Further exploration
What changes if one short link appears on a live television broadcast? Follow that single hot key through the cache, database, and analytics pipeline. Then consider a link whose owner changes its destination: decide how quickly existing cache entries must stop returning the old address.
Interview playbook
Interview Tips
Start with requirements clarification — Don't jump into design. Ask about scale, custom aliases, analytics, expiration.
Lead with the key generation discussion — This is the core algorithmic challenge. Discuss all three approaches, tradeoffs, and recommend KGS.
Separate read and write paths — They have very different scale (100:1 ratio). This shows you think about system characteristics.
Don't forget caching — With 100:1 read:write ratio, caching is critical. Discuss eviction, warming, and hot key handling.
Mention 301 vs 302 — This is a subtle but important tradeoff that shows depth.
Discuss cleanup of expired URLs — Shows you think about long-term system health and operational concerns.
End with reliability — Walk through failure scenarios. "What happens if X goes down?" for each component.