After practicing system design concepts for a while, I finally decided to apply them by building real projects. This URL shortener is my first assignment, where I focused on translating high-level design ideas into a working systemβcovering architecture, caching, databases, and scalability considerations. The goal was not just to build a feature, but to think through the trade-offs and design decisions that matter in production systems.
A URL shortener is one of those systems that looks trivial at first glance but reveals deep engineering challenges once we design it for scale, reliability, and performance.
In this blog, weβll design a URL shortener end-to-end:
- What a URL shortener is and why we need it
- High-Level Design (HLD)
- Database and caching strategy
- Browser cache & HTTP redirects
- Back-of-the-envelope calculations
- A clean Node.js implementation with code
1οΈβ£ What Is a URL Shortener?
A URL shortener converts a long URL into a compact alias.
Example
Long URL:
https://www.example.com/products/electronics/mobiles/samsung/galaxy-s23-ultra
Short URL:
https://sho.rt/aZ9kQ
When someone opens the short URL, the system:
- Finds the original long URL
- Redirects the user to it
At its core, this is a keyβvalue mapping:
short_code β long_url
2οΈβ£ Why Do We Need URL Shorteners?
πΉ Better User Experience
Long URLs are hard to read, remember, and share.
πΉ Platform Constraints
SMS, social media posts, QR codes, and print media benefit from shorter links.
πΉ Analytics
We can track:
- Click counts
- Time
- Location
- Device / browser
πΉ Link Management
We gain the ability to:
- Expire links
- Disable links
- Rotate destinations
- Run A/B tests
3οΈβ£ High-Level Design (HLD)
π§± Logical Architecture
πΉ Why Each Component Exists
- Browser Cache β Fastest cache (used carefully)
- CDN β Global low latency
- Load Balancer β Traffic distribution & failover
- Service Layer β Stateless business logic
- Redis β Ultra-fast lookups for hot links
- Database β Durable storage
4οΈβ£ Database Design
π¦ Data Model
| Field | Description |
|---|---|
| short_code | Primary key |
| long_url | Original URL |
| created_at | Creation time |
| expiry_at | Optional |
| click_count | Optional |
ποΈ SQL vs NoSQL
- SQL β Simpler, strong consistency
- NoSQL (DynamoDB / Cassandra) β Horizontal scale, high throughput
π At scale, this is a NoSQL key-value workload:
Partition Key = short_code
5οΈβ£ Server-Side Cache (Redis)
Redirect traffic is ~99% reads.
π₯ Why Redis?
- Memory-level speed
- Offloads database
- Handles hot links efficiently
β‘ Redirect Flow
Request
β
Redis hit β Redirect
Redis miss β DB lookup β Cache β Redirect
6οΈβ£ Browser Cache (Important Trade-off)
Browser cache sits before our servers and can completely bypass them.
Redirect Status Codes Matter
| Status | Browser Behavior |
|---|---|
| 301 | Cached aggressively |
| 302 / 307 | Revalidated / not cached |
Typical Production Strategy
We usually disable browser caching:
Cache-Control: no-store, no-cache, must-revalidate
Why?
- We donβt lose analytics
- We can revoke links
- We can rotate destinations
π Speed comes from Redis + CDN, not browser cache.
7οΈβ£ Short Code Generation
Base62 Encoding
Characters:
aβz AβZ 0β9 (62 chars)
Benefits:
- URL-safe
- Compact
- Deterministic
Example:
ID: 125 β cb
8οΈβ£ Back-of-the-Envelope Calculations
Assumptions
- DAU: 10 million
- URLs created/user/day: 0.1
- Redirects per URL/day: 50
Write Traffic
1M URLs/day β 12 writes/sec
Read Traffic
50M redirects/day β 580 RPS
Peak β 3,000 RPS
π System is read-heavy, so caching dominates design.
Storage
~260 bytes / URL
365M URLs/year β 95 GB/year
Manageable with NoSQL.
Redis Cache
10M hot URLs Γ 300 bytes β 3 GB
9οΈβ£ Node.js Implementation
π Project Structure
src/
βββ app.ts
βββ controllers/
β βββ url.controller.ts
βββ services/
β βββ url.service.ts
βββ repositories/
β βββ url.repository.ts
βββ cache/
β βββ redis.client.ts
βββ utils/
βββ base62.ts
πΉ Base62 Encoder
// utils/base62.ts
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
export function encodeBase62(num: number): string {
let result = "";
while (num > 0) {
result = chars[num % 62] + result;
num = Math.floor(num / 62);
}
return result;
}
πΉ Redis Client
// cache/redis.client.ts
import Redis from "ioredis";
export const redis = new Redis({
host: "localhost",
port: 6379
});
πΉ Repository Layer (DB abstraction)
// repositories/url.repository.ts
const db = new Map<string, string>(); // mock DB
export class UrlRepository {
async save(shortCode: string, longUrl: string) {
db.set(shortCode, longUrl);
}
async findByShortCode(code: string): Promise<string | null> {
return db.get(code) || null;
}
}
πΉ Service Layer (Core Logic)
// services/url.service.ts
import { UrlRepository } from "../repositories/url.repository";
import { redis } from "../cache/redis.client";
import { encodeBase62 } from "../utils/base62";
let globalId = 100000;
export class UrlService {
private repo = new UrlRepository();
async createShortUrl(longUrl: string): Promise<string> {
const id = globalId++;
const shortCode = encodeBase62(id);
await this.repo.save(shortCode, longUrl);
await redis.set(shortCode, longUrl);
return shortCode;
}
async getLongUrl(shortCode: string): Promise<string | null> {
const cached = await redis.get(shortCode);
if (cached) return cached;
const longUrl = await this.repo.findByShortCode(shortCode);
if (!longUrl) return null;
await redis.set(shortCode, longUrl);
return longUrl;
}
}
πΉ Controller Layer
// controllers/url.controller.ts
import { Request, Response } from "express";
import { UrlService } from "../services/url.service";
const service = new UrlService();
export async function shortenUrl(req: Request, res: Response) {
const { longUrl } = req.body;
const code = await service.createShortUrl(longUrl);
res.json({
shortUrl: `https://sho.rt/${code}`
});
}
export async function redirect(req: Request, res: Response) {
const { code } = req.params;
const longUrl = await service.getLongUrl(code);
if (!longUrl) {
return res.status(404).send("Not found");
}
res.setHeader("Cache-Control", "no-store");
res.redirect(302, longUrl);
}
πΉ App Entry Point
// app.ts
import express from "express";
import { shortenUrl, redirect } from "./controllers/url.controller";
const app = express();
app.use(express.json());
app.post("/shorten", shortenUrl);
app.get("/:code", redirect);
app.listen(3000, () => {
console.log("URL Shortener running on port 3000");
});
2.GET /code
π Final Thoughts
A URL shortener is a perfect system design problem because it tests:
- Data modeling
- Caching strategy (browser, CDN, Redis)
- HTTP semantics
- Scale estimation
- Clean backend architecture
The biggest lesson:
The fastest systems are not the ones with the most caches β
but the ones that cache at the right layers.



Top comments (1)
Great to read: systemdesignschool.io/problems/url...