DEV Community

Cover image for πŸ”— Designing a URL Shortener: From Architecture to Node.js Implementation
Abhinav
Abhinav

Posted on

πŸ”— Designing a URL Shortener: From Architecture to Node.js Implementation

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
Enter fullscreen mode Exit fullscreen mode

When someone opens the short URL, the system:

  1. Finds the original long URL
  2. Redirects the user to it

At its core, this is a key–value mapping:

short_code β†’ long_url
Enter fullscreen mode Exit fullscreen mode

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)

High-Level Design URL shortner

🧱 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • URL-safe
  • Compact
  • Deterministic

Example:

ID: 125 β†’ cb
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Read Traffic

50M redirects/day β‰ˆ 580 RPS
Peak β‰ˆ 3,000 RPS
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ System is read-heavy, so caching dominates design.


Storage

~260 bytes / URL
365M URLs/year β‰ˆ 95 GB/year
Enter fullscreen mode Exit fullscreen mode

Manageable with NoSQL.


Redis Cache

10M hot URLs Γ— 300 bytes β‰ˆ 3 GB
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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;
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή Redis Client

// cache/redis.client.ts
import Redis from "ioredis";

export const redis = new Redis({
  host: "localhost",
  port: 6379
});
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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");
});
Enter fullscreen mode Exit fullscreen mode

1.POST /shorten
shorten url

2.GET /code

redirection 302

πŸ”Ÿ 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)

Collapse
 
abhivyaktii profile image
Abhinav