Backend System Design Interview: Design a URL Shortener
Backend System Design Interview: Design a URL Shortener
If you’ve ever prepared for a senior backend engineering interview, you’ve likely encountered the “Design a URL Shortener” problem. It’s a classic system design question—deceptively simple on the surface, but rich with complexity when you dig deeper. In this post, I’ll walk you through a complete approach to designing a URL shortener like TinyURL or bit.ly, covering everything from requirements gathering to scaling considerations.
Why This Problem Matters
URL shorteners are ubiquitous. They power link sharing on Twitter, SMS marketing, and analytics tracking. But beneath the simple act of shortening a URL lies a fascinating set of engineering challenges:
- High write throughput: Millions of new URLs created daily
- Extremely high read throughput: Billions of redirects per day
- Low latency requirements: Every redirect should happen in under 100ms
- Data integrity: No two long URLs should map to different short URLs (unless designed for analytics)
- Scalability: The system must handle spikes (e.g., Super Bowl ads)
Step 1: Requirements Gathering
Before writing a single line of code, clarify the requirements with your interviewer. This demonstrates your ability to think critically about trade-offs.
Functional Requirements
- Shorten: Given a long URL, generate a unique, short alias
- Redirect: Given a short URL, redirect the user to the original long URL
- Custom aliases: (Optional) Allow users to specify their own short path
- TTL/Expiration: (Optional) URLs can expire after a certain time
- Analytics: (Optional) Track click counts, referrers, geolocation
Non-Functional Requirements
- High availability: The system should never go down
- Low latency: Redirects in < 100ms
- Scalability: Handle billions of requests per month
- Durability: Once a short URL is created, it should never be lost
Out of Scope (for this interview)
- User authentication
- Spam detection
- Advanced analytics dashboards
Step 2: Traffic and Storage Estimation
Always estimate scale. This shows you understand real-world constraints.
Assumptions:
- 100 million new URLs per month
- 10 billion redirects per month
- Average URL length: 100 characters
- Short URL length: 6 characters (alphanumeric: 62^6 ≈ 56 billion combinations)
Storage Calculation:
- Each mapping: 100 bytes (long URL) + 6 bytes (short key) + 8 bytes (creation timestamp) + overhead ≈ 200 bytes
- Monthly: 100 million × 200 bytes = 20 GB
- 5 years: 20 GB × 12 × 5 = 1.2 TB
Bandwidth:
- Writes: 100 million / (30 × 24 × 3600) ≈ 38 writes/second
- Reads: 10 billion / (30 × 24 × 3600) ≈ 3,800 reads/second
- Peak: Assume 10× spike → 38,000 reads/second
Step 3: API Design
Let’s design a clean REST API.
Create Short URL
1 | POST /api/shorten |
Redirect
1 | GET /{shortKey} |
Why 301 vs 302?
- 301 (Moved Permanently): Browser caches the redirect, reducing load on servers
- 302 (Found): For analytics, use 302 to track each click
Step 4: Database Schema
We need a simple but efficient schema.
1 | CREATE TABLE url_mappings ( |
Database Choice:
- SQL (PostgreSQL/MySQL): For strong consistency and transactions
- NoSQL (Cassandra/DynamoDB): For extreme scalability, but eventual consistency might cause issues
Trade-off: I’d recommend SQL for the primary store because we need strong consistency for short key uniqueness. Use NoSQL for caching or analytics.
Step 5: Short Key Generation
This is the most interesting algorithmic challenge. We need to generate unique, short, and random-looking keys.
Approach 1: Hash + Base62 Encoding
1 | import java.security.MessageDigest; |
Pros: Deterministic, no DB lookup needed Cons: Collisions possible, need to handle with retry or append salt
Approach 2: Distributed Unique ID + Base62
Use a distributed ID generator (like Snowflake) and encode it in Base62.
1 | public class SnowflakeIdGenerator { |
Pros: No collisions, fast, scalable Cons: Need coordination for machine IDs
Step 6: System Architecture
Let’s put it all together with a high-level design.
1 | [Client] → [Load Balancer] → [Web Servers] → [Cache (Redis)] → [Database (PostgreSQL)] |
Components
- Load Balancer: Distributes traffic across web servers (NGINX, HAProxy)
- Web Servers: Stateless application servers (Spring Boot, Node.js)
- Cache: Redis cluster for hot URLs (LRU eviction)
- Database: PostgreSQL with replication for durability
- Key Generation Service: Generates unique short keys
Data Flow
Creating a short URL:
- Client sends POST request with long URL
- Web server calls Key Generation Service
- Web server inserts mapping into DB
- Web server caches mapping in Redis
- Returns short URL to client
Redirect flow:
- Client sends GET request with short key
- Web server checks Redis cache
- If miss, query DB
- Cache the result in Redis (with TTL)
- Return 301 redirect to long URL
Step 7: Caching Strategy
Caching is critical for read-heavy workloads.
1 | # Redis Configuration |
Cache-Aside Pattern:
1 | public String getLongUrl(String shortKey) { |
Cache Hit Ratio: With proper sizing, expect 95%+ hit rate for popular URLs.
Step 8: Handling Edge Cases
Collision Handling
1 | public String createShortUrl(String longUrl, String customAlias) { |
Expiration
Implement a background job to clean expired URLs:
1 |
|
Step 9: Scaling Considerations
Database Scaling
- Read replicas: Offload read traffic to replicas
- Sharding: Partition by short key hash (consistent hashing)
- Connection pooling: Use HikariCP for efficient connections
1 | # Sharding configuration |
Rate Limiting
Protect against abuse:
1 | public class RateLimiter { |
Monitoring
Essential metrics to track:
- QPS (reads and writes)
- Cache hit ratio
- Database query latency (p99)
- Error rates (collision, timeout)
- Expired URL count
Step 10: Alternative Designs and Trade-offs
Design A: Single Database + Cache
Pros: Simple, consistent Cons: Database becomes bottleneck at scale
Design B: Distributed Key-Value Store
Pros: Highly scalable, low latency Cons: Eventual consistency, complex operations
Design C: Pre-generated Keys Pool
Pros: Fast key generation, no collisions Cons: Wasteful if keys not used, need to manage pool size
My recommendation: Start with Design A for MVP, evolve to Design B as traffic grows.
Key Takeaways
- Start with requirements: Always clarify functional and non-functional requirements before diving into design.
- Estimate scale: Calculate storage, bandwidth, and QPS to guide architecture decisions.
- Key generation is critical: Choose between hash-based (simple) or distributed ID (scalable) approaches.
- Cache aggressively: With a 95%+ read-to-write ratio, caching is your best performance lever.
- Plan for collisions: Implement retry logic and unique constraints to handle edge cases.
- Think about expiration: Background jobs for cleanup are essential for long-term maintenance.
- Rate limiting is non-negotiable: Protect your system from abuse and DDoS attacks.
- Monitor everything: Without observability, you’re flying blind in production.
Remember: In a system design interview, the goal isn’t to produce a perfect design—it’s to demonstrate your thought process, trade-off analysis, and ability to communicate complex ideas clearly. Practice this problem with different variations, and you’ll be well-prepared for the real thing.