
URL shorteners like Bit.ly or TinyURL are deceptively simple: take a long URL, generate a short one, and redirect. But when you want to scale to billions of URLs with low latency, careful system design is crucial. In this article, we’ll break down a full architecture for a production-ready URL shortener.
1. Requirements
Before we design, let’s define the requirements:
Functional Requirements:
- Create short URLs from long URLs.
- Redirect short URLs to the original URLs.
- Support custom / vanity URLs.
- Track analytics (clicks, unique users).
Non-Functional Requirements:
- High availability (99.9% uptime).
- Low latency for redirects (<50ms median).
- Scalability to billions of URLs.
- Security (rate limiting, malware detection).
2. Generating Unique Short IDs
We need a unique identifier for each URL. There are multiple approaches:
- Auto-incrementing IDs: easy but predictable.
- Random IDs: harder to guess.
- Snowflake IDs: distributed, sortable, globally unique.
Snowflake IDs
Snowflake is a 64-bit unique ID generator originally used by Twitter.
| Bits | Meaning |
|---|---|
| 1 | Sign bit (unused) |
| 41 | Timestamp in milliseconds since custom epoch |
| 10 | Machine ID (datacenter + worker) |
| 12 | Sequence number per millisecond |
Example Calculation:
Timestamp = 100,000 ms since epoch
Machine ID = 17
Sequence = 25
ID = (100000 << 22) | (17 << 12) | 25
= 419,430,469,657
3. Base62 Encoding
The Snowflake ID is a large integer. To make it URL-friendly, we encode it in Base62 (0-9, A-Z, a-z):
Step-by-Step:
- Divide the integer by 62, store remainder as character.
- Repeat until quotient = 0.
- Reverse remainders → short string.
Example:
ID = 419,430,469,657 → Base62 = "7LLBP0o"
This short string becomes the short URL:
https://short.ly/7LLBP0o
4. High-Level Architecture
Here’s the architecture of our system:
┌───────────────┐
│ User │
│(Browser/App) │
└───────┬───────┘
│ POST /shorten or GET /s/:id
▼
┌───────────────┐
│ API Gateway │
│ (Auth, Rate │
│ Limiting, LB)│
└───────┬───────┘
│
┌───────────┴───────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Shortlink │ │ Analytics │
│ Service │ │ Service │
│ - Validates │ │ - Consumes │
│ - Generates │ │ click events │
│ Snowflake ID│ │ - Aggregates │
│ - Base62 enc │ │ - Stores in OLAP│
│ - Stores DB │ │ │
│ - Updates Redis│ │ │
└─────┬─────────┘ └───────────────┘
│
▼
┌───────────────┐
│ Redis Cache │ ← Hot cache for redirects
└─────┬─────────┘
▼
┌───────────────┐
│ Primary DB │ ← Source-of-truth
│ shortlinks │
│ schema: │
│ - short_id │
│ - original_url│
│ - owner_id │
│ - created_at │
└───────────────┘
5. Detailed Flow
Creating a Short URL
- User sends
POST /shortenwith URL (and optional custom alias). - API Gateway authenticates, enforces rate limits, forwards request to Shortlink Service.
- Service generates a Snowflake ID, converts it to Base62, and stores it in DB and Redis cache.
- Returns
https://short.ly/{short_id}.
Redirecting a Short URL
- User clicks
GET /s/{short_id}. - CDN / Redis cache checked for mapping.
- Cache hit → HTTP 301 redirect.
- Cache miss → query DB → update cache → redirect.
- Push click event to Kafka for analytics.
6. Handling Custom / Vanity URLs
- Validate input (allowed characters, reserved words).
- Ensure uniqueness using DB constraint.
- For collisions, return error or ask user to choose another alias.
7. Scaling Considerations
- Microservices: Shortlink Service, Analytics Service, User Service, etc.
- DB sharding: shard by
short_idhash to scale billions of URLs. - Caching: Redis + CDN to reduce latency.
- Analytics: Kafka for asynchronous event processing.
- Snowflake IDs: distributed unique ID generation across multiple servers.
8. JavaScript Example
// Base62 encoding
const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
function encodeBase62(num) {
if (num === 0) return '0';
let str = '';
while (num > 0) {
str = BASE62[num % 62] + str;
num = Math.floor(num / 62);
}
return str;
}
// Simplified Snowflake ID generator
class Snowflake {
constructor(machineId = 1) {
this.machineId = machineId & 0x3FF;
this.sequence = 0;
this.lastTimestamp = -1;
this.epoch = 1700000000000;
}
currentTime() { return Date.now() - this.epoch; }
nextId() {
let timestamp = this.currentTime();
if (timestamp === this.lastTimestamp) this.sequence = (this.sequence + 1) & 0xFFF;
else this.sequence = 0;
this.lastTimestamp = timestamp;
return (BigInt(timestamp) << 22n) | (BigInt(this.machineId) << 12n) | BigInt(this.sequence);
}
}
// Usage
const snowflake = new Snowflake(1);
const id = snowflake.nextId();
const shortId = encodeBase62(Number(id % BigInt(Number.MAX_SAFE_INTEGER)));
console.log('Short ID:', shortId);
9. Key Takeaways
- Snowflake IDs + Base62 → scalable, unique, URL-friendly IDs.
- Cache first (Redis/CDN) → low-latency redirects.
- Microservices + Sharding → handle billions of URLs.
- Asynchronous analytics → decoupled pipeline for clicks.
- Custom aliases → validated and constrained in DB.
This architecture can support high availability, billions of URLs, and low-latency redirects, making it suitable for production systems like Bit.ly.
List of Some important Leet code Questions:
- Leet code 799. Champagne Tower
- LeetCode 389. Find The Difference
- Leetcode 775. Find The Global and Local Inversions
- Leetcode 316. Remove Duplicate Letters
- LeetCode 2233 Maximum Product After K Increments
- LeetCode 880. Decoded String at Index
- LeetCode 905. Sort Array By Parity
- LeetCode 896. Monotonic Array
- LeetCode 132. Pattern
- LeetCode 557. Reverse Words in a String III (easy)
- Leetcode 2038. Remove Colored Pieces if Both Neighbors are the Same Color
- Leetcode 1512. Number of Good Pairs (easy)
- Leetcode 706. Design HashMap (Easy)
- LeetCode 229. Majority Element II (Medium)
SystemDesign#BackendEngineering#SoftwareArchitecture#Microservices#Redis#Kafka#NodeJS#Scalability#BackendDeveloper#FullStackEngineer#CloudArchitecture#WebPerformance#InterviewPrep#DistributedSystems#EngineeringDesign



If you’ve built a URL shortener, what storage did you use for lookups (RDBMS, Redis, Cassandra)? Why that choice?
Why choose Snowflake-style IDs vs. a simple auto-increment in the DB?
Snowflake-style IDs enable distributed ID generation without a single DB write per new short URL, avoiding a global hotspot/bottleneck and allowing very high write throughput. They also embed time + node info which can help sorting and debugging. The tradeoffs: you need to manage clock skew and pick node IDs carefully; IDs are larger than simple auto-increments so your resulting encoded strings might be longer unless you tune bits vs. base encoding.
How do you guarantee no collisions when using Base62 encoding of numeric IDs?
Base62 is just an encoding — it’s bijective for integers, so collisions only happen if two different numeric IDs map to the same encoded string, which can’t happen. Real collisions come from two different systems generating the same numeric ID. Avoid that by ensuring a global uniqueness strategy: Snowflake reserves node bits and timestamp bits, or use centralized allocation. Also always verify uniqueness at creation as a safety net (cheap extra DB index check).
How do you handle clock skew in Snowflake IDs?
Common patterns:
If the local clock jumps backwards, stall ID generation until the clock catches up (simple but stalls).
Use a per-node sequence extension to continue issuing IDs for the prior timestamp range (requires careful sequence rollover).
Use NTP/chrony to minimize skew and monitor time drift aggressively.
For multi-region, prefer logical time or vector-clock style solutions if monotonic ordering across regions is critical.
What storage is best for lookup performance at scale?
Many systems use:
You can combine: write-through cache with TTLs, or use DynamoDB with DAX (cache). The important part is connection pooling, local caches at CDN/edge, and a simple read path to minimize redirect latency.
How many characters should a short URL be (tradeoff between length & info)?
It depends on expected QPS and lifetime:
For purely incremental integer IDs encoded in Base62: length ≈ log_base62(max_id).