Skip to content

কেস স্টাডি: স্কেলে URL Shortener

base62 encoding, Snowflake ID, caching, rate limiting এবং analytics সহ একটি production URL shortener ডিজাইন করে বানান।

গল্পে বুঝি

ইবনে সিনার ভাইয়ের বিয়ের অনুষ্ঠান, গেটের পাশেই কোট-ব্যাগ জমা রাখার একটা কাউন্টার। আল-খোয়ারিজমি ভেতরে ঢোকার আগে তার বিশাল ভারী ব্যাগটা কাউন্টারে জমা দিল, বিনিময়ে হাতে পেল ছোট্ট একটা নম্বরওয়ালা টোকেন — “৪৭”। কাউন্টারের ছেলেটা তার খাতায় টুকে রাখল: টোকেন ৪৭ মানে ৩ নম্বর তাকের ব্যাগ। আল-খোয়ারিজমি নিশ্চিন্তে গিয়ে খাওয়াদাওয়া করল, পকেটে শুধু ওই ছোট টোকেন। ফেরার সময় সে টোকেনটা দেখাতেই ছেলেটা খাতা দেখে সঙ্গে সঙ্গে ৩ নম্বর তাক থেকে ব্যাগটা এনে হাতে ধরিয়ে দিল — এক সেকেন্ডও লাগল না।

মজার ব্যাপার হলো, কোনো দুইজনের টোকেন এক হতে পারবে না — এক হলেই তো ভুল ব্যাগ চলে যাবে, তাই প্রতিটা টোকেন আলাদা। আর অনুষ্ঠানের হোস্ট ফাতিমা আল-ফিহরি যেহেতু বারবার ভেতর-বাইরে করছে, ছেলেটা তার টোকেনটা আর খাতায় খুঁজতেও যায় না — মুখস্থ হয়ে গেছে কোন তাকে, চোখের পলকে বের করে দেয়।

আসলে এটাই একটা URL shortener। ভারী ব্যাগটা হলো লম্বা URL, ছোট টোকেনটা হলো short code, আর কাউন্টারের খাতাটা হলো key-value store যেখানে টোকেন-থেকে-URL এর mapping রাখা থাকে। টোকেন দেখিয়ে ব্যাগ ফেরত পাওয়াটাই lookup আর redirect, প্রতিটা টোকেন আলাদা রাখাটাই unique code generation, আর হোস্টের টোকেন মুখস্থ রাখাটাই hot link-এর cache। bit.ly বা TinyURL ঠিক এভাবেই একটা লম্বা লিংককে ছোট code-এ বদলে দেয়, আর কেউ ওই ছোট লিংকে ক্লিক করলে মুহূর্তেই মূল ঠিকানায় পৌঁছে দেয়।

ইন্টারভিউয়ের ক্লাসিক প্রশ্ন — এবার সত্যিকারভাবে

URL shortener প্রতিটি system design interview-তে আসে। কিন্তু বেশিরভাগ ব্যাখ্যা “একটা hash ব্যবহার করো” বলেই থেমে যায়। এখানে আমরা পুরো সিস্টেমটা বানাব: distributed ID generation, encoding, caching, rate limiting, analytics এবং storage — সবই production-ready।

বাস্তব জীবনের উদাহরণ

থিয়েটারের একটি coat check-এর মতো — আপনি একটা লম্বা URL জমা দেন আর একটা ছোট ticket number পান। পরে ticket দেখালে আপনার মূল URL ফেরত পান।

প্রয়োজনীয়তা

  • Functional: short URL বানানো, মূল URL-এ redirect করা, click analytics track করা
  • Non-functional: প্রতি সেকেন্ডে 1000 URL creation, প্রতি সেকেন্ডে 100K redirect, 99.9% uptime, 10ms-এর নিচে redirect latency
  • Storage: 100M URL, ~10GB data + analytics
URL Shortener Architecture

সম্পূর্ণ সিস্টেম

import http from 'node:http';
import crypto from 'node:crypto';

// ===========================================
// 1. SNOWFLAKE ID GENERATOR
// ===========================================
class SnowflakeIDGenerator {
	private sequence = 0n;
	private lastTimestamp = -1n;
	private readonly epoch = 1700000000000n; // custom epoch
	private readonly workerIdBits = 10n;
	private readonly sequenceBits = 12n;
	private readonly maxSequence = (1n << this.sequenceBits) - 1n;
	private readonly workerIdShift = this.sequenceBits;
	private readonly timestampShift = this.sequenceBits + this.workerIdBits;

	constructor(private readonly workerId: bigint) {
		if (workerId < 0n || workerId >= 1n << this.workerIdBits) {
			throw new Error(`Worker ID must be between 0 and ${(1n << this.workerIdBits) - 1n}`);
		}
	}

	generate(): bigint {
		let timestamp = BigInt(Date.now()) - this.epoch;

		if (timestamp === this.lastTimestamp) {
			this.sequence = (this.sequence + 1n) & this.maxSequence;
			if (this.sequence === 0n) {
				// Wait for next millisecond
				while (timestamp <= this.lastTimestamp) {
					timestamp = BigInt(Date.now()) - this.epoch;
				}
			}
		} else {
			this.sequence = 0n;
		}

		this.lastTimestamp = timestamp;

		return (
			(timestamp << this.timestampShift) | (this.workerId << this.workerIdShift) | this.sequence
		);
	}
}

// ===========================================
// 2. BASE62 ENCODER
// ===========================================
const BASE62_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

function base62Encode(num: bigint): string {
	if (num === 0n) return BASE62_CHARS[0];
	let result = '';
	while (num > 0n) {
		result = BASE62_CHARS[Number(num % 62n)] + result;
		num = num / 62n;
	}
	return result;
}

function base62Decode(str: string): bigint {
	let result = 0n;
	for (const char of str) {
		result = result * 62n + BigInt(BASE62_CHARS.indexOf(char));
	}
	return result;
}

// ===========================================
// 3. STORAGE (in-memory, replace with PG)
// ===========================================
interface URLRecord {
	id: string;
	shortCode: string;
	originalUrl: string;
	userId: string;
	createdAt: string;
	expiresAt: string | null;
	clickCount: number;
}

// In production: PostgreSQL table
// CREATE TABLE urls (
//   id BIGINT PRIMARY KEY,
//   short_code VARCHAR(11) UNIQUE NOT NULL,
//   original_url TEXT NOT NULL,
//   user_id VARCHAR(50),
//   created_at TIMESTAMPTZ DEFAULT NOW(),
//   expires_at TIMESTAMPTZ,
//   click_count BIGINT DEFAULT 0
// );
// CREATE INDEX idx_urls_short_code ON urls(short_code);
// CREATE INDEX idx_urls_user_id ON urls(user_id);

const urlStore = new Map<string, URLRecord>();
const codeToId = new Map<string, string>();

// ===========================================
// 4. CACHE (in-memory, replace with Redis)
// ===========================================
class LRUCache {
	private cache = new Map<string, { value: string; expiry: number }>();
	private maxSize: number;

	constructor(maxSize = 10000) {
		this.maxSize = maxSize;
	}

	get(key: string): string | null {
		const entry = this.cache.get(key);
		if (!entry) return null;
		if (Date.now() > entry.expiry) {
			this.cache.delete(key);
			return null;
		}
		// Move to end (most recently used)
		this.cache.delete(key);
		this.cache.set(key, entry);
		return entry.value;
	}

	set(key: string, value: string, ttlMs: number): void {
		if (this.cache.size >= this.maxSize) {
			// Evict oldest entry
			const firstKey = this.cache.keys().next().value;
			if (firstKey) this.cache.delete(firstKey);
		}
		this.cache.set(key, { value, expiry: Date.now() + ttlMs });
	}

	delete(key: string): void {
		this.cache.delete(key);
	}
}

// ===========================================
// 5. RATE LIMITER (in-memory sliding window)
// ===========================================
class RateLimiter {
	private windows = new Map<string, number[]>();

	isAllowed(key: string, maxRequests: number, windowMs: number): boolean {
		const now = Date.now();
		const timestamps = this.windows.get(key) || [];

		// Remove expired entries
		const valid = timestamps.filter((t) => t > now - windowMs);
		valid.push(now);
		this.windows.set(key, valid);

		return valid.length <= maxRequests;
	}
}

// ===========================================
// 6. ANALYTICS (async click tracking)
// ===========================================
interface ClickEvent {
	shortCode: string;
	timestamp: string;
	ip: string;
	userAgent: string;
	referer: string;
}

class AnalyticsCollector {
	private buffer: ClickEvent[] = [];
	private flushInterval: ReturnType<typeof setInterval>;

	constructor() {
		// Flush every 5 seconds (in production: write to Kafka/SQS)
		this.flushInterval = setInterval(() => this.flush(), 5000);
	}

	track(event: ClickEvent): void {
		this.buffer.push(event);
	}

	private flush(): void {
		if (this.buffer.length === 0) return;
		const batch = this.buffer.splice(0);
		// In production: send to analytics pipeline
		console.log(`[Analytics] Flushed ${batch.length} click events`);
	}

	stop(): void {
		clearInterval(this.flushInterval);
		this.flush();
	}
}

// ===========================================
// 7. URL SHORTENER SERVICE
// ===========================================
class URLShortenerService {
	private idGen: SnowflakeIDGenerator;
	private cache: LRUCache;
	private rateLimiter: RateLimiter;
	private analytics: AnalyticsCollector;
	private readonly baseUrl: string;

	constructor(workerId: number, baseUrl: string) {
		this.idGen = new SnowflakeIDGenerator(BigInt(workerId));
		this.cache = new LRUCache(50000);
		this.rateLimiter = new RateLimiter();
		this.analytics = new AnalyticsCollector();
		this.baseUrl = baseUrl;
	}

	// Create short URL
	async createShortURL(
		originalUrl: string,
		userId: string,
		expiresIn?: number
	): Promise<{
		shortUrl: string;
		shortCode: string;
	}> {
		// Validate URL
		try {
			new URL(originalUrl);
		} catch {
			throw new Error('Invalid URL format');
		}

		// Check for existing URL (deduplication)
		for (const record of urlStore.values()) {
			if (record.originalUrl === originalUrl && record.userId === userId) {
				return {
					shortUrl: `${this.baseUrl}/${record.shortCode}`,
					shortCode: record.shortCode
				};
			}
		}

		// Generate unique ID using Snowflake
		const id = this.idGen.generate();
		const shortCode = base62Encode(id);

		const record: URLRecord = {
			id: id.toString(),
			shortCode,
			originalUrl,
			userId,
			createdAt: new Date().toISOString(),
			expiresAt: expiresIn ? new Date(Date.now() + expiresIn * 1000).toISOString() : null,
			clickCount: 0
		};

		// Store in DB
		urlStore.set(record.id, record);
		codeToId.set(shortCode, record.id);

		// Pre-warm cache
		this.cache.set(shortCode, originalUrl, 3600000); // 1 hour

		return {
			shortUrl: `${this.baseUrl}/${shortCode}`,
			shortCode
		};
	}

	// Resolve short URL (redirect)
	async resolve(
		shortCode: string,
		ip: string,
		userAgent: string,
		referer: string
	): Promise<string | null> {
		// 1. Check cache
		const cached = this.cache.get(shortCode);
		if (cached) {
			this.trackClick(shortCode, ip, userAgent, referer);
			return cached;
		}

		// 2. Check database
		const id = codeToId.get(shortCode);
		if (!id) return null;

		const record = urlStore.get(id);
		if (!record) return null;

		// Check expiration
		if (record.expiresAt && new Date(record.expiresAt) < new Date()) {
			return null;
		}

		// Update cache
		this.cache.set(shortCode, record.originalUrl, 3600000);

		// Track click (async, don't block redirect)
		this.trackClick(shortCode, ip, userAgent, referer);
		record.clickCount++;

		return record.originalUrl;
	}

	private trackClick(shortCode: string, ip: string, userAgent: string, referer: string): void {
		this.analytics.track({
			shortCode,
			timestamp: new Date().toISOString(),
			ip,
			userAgent,
			referer
		});
	}

	// Get analytics
	async getStats(shortCode: string): Promise<URLRecord | null> {
		const id = codeToId.get(shortCode);
		if (!id) return null;
		return urlStore.get(id) || null;
	}

	checkRateLimit(ip: string): boolean {
		return this.rateLimiter.isAllowed(ip, 10, 60000); // 10 req/min
	}

	shutdown(): void {
		this.analytics.stop();
	}
}

// ===========================================
// 8. HTTP SERVER
// ===========================================
const service = new URLShortenerService(
	parseInt(process.env.WORKER_ID || '1'),
	process.env.BASE_URL || 'http://localhost:3000'
);

function parseBody(req: http.IncomingMessage): Promise<unknown> {
	return new Promise((resolve, reject) => {
		const chunks: Buffer[] = [];
		req.on('data', (c) => chunks.push(c));
		req.on('end', () => {
			try {
				resolve(JSON.parse(Buffer.concat(chunks).toString()));
			} catch {
				reject(new Error('Invalid JSON'));
			}
		});
	});
}

function json(res: http.ServerResponse, status: number, data: unknown): void {
	res.writeHead(status, { 'Content-Type': 'application/json' });
	res.end(JSON.stringify(data));
}

const server = http.createServer(async (req, res) => {
	const url = new URL(req.url || '/', `http://${req.headers.host}`);
	const method = req.method || 'GET';
	const ip = req.socket.remoteAddress || 'unknown';

	try {
		// Rate limiting
		if (!service.checkRateLimit(ip)) {
			json(res, 429, { error: 'Too many requests' });
			return;
		}

		// POST /api/shorten — Create short URL
		if (url.pathname === '/api/shorten' && method === 'POST') {
			const body = (await parseBody(req)) as { url: string; userId?: string; expiresIn?: number };

			if (!body.url) {
				json(res, 400, { error: 'url is required' });
				return;
			}

			const result = await service.createShortURL(
				body.url,
				body.userId || 'anonymous',
				body.expiresIn
			);
			json(res, 201, result);
			return;
		}

		// GET /api/stats/:code — Get analytics
		const statsMatch = url.pathname.match(/^\/api\/stats\/([a-zA-Z0-9]+)$/);
		if (statsMatch && method === 'GET') {
			const stats = await service.getStats(statsMatch[1]);
			if (!stats) {
				json(res, 404, { error: 'URL not found' });
				return;
			}
			json(res, 200, stats);
			return;
		}

		// GET /:code — Redirect
		const codeMatch = url.pathname.match(/^\/([a-zA-Z0-9]+)$/);
		if (codeMatch && method === 'GET') {
			const originalUrl = await service.resolve(
				codeMatch[1],
				ip,
				req.headers['user-agent'] || '',
				req.headers.referer || ''
			);

			if (!originalUrl) {
				json(res, 404, { error: 'Short URL not found or expired' });
				return;
			}

			res.writeHead(301, { Location: originalUrl, 'Cache-Control': 'private, max-age=90' });
			res.end();
			return;
		}

		// Health check
		if (url.pathname === '/health') {
			json(res, 200, { status: 'ok' });
			return;
		}

		json(res, 404, { error: 'Not found' });
	} catch (err) {
		console.error('Error:', err);
		json(res, 500, { error: 'Internal server error' });
	}
});

const PORT = parseInt(process.env.PORT || '3000');
server.listen(PORT, () => console.log(`URL Shortener on http://localhost:${PORT}`));

process.on('SIGTERM', () => {
	service.shutdown();
	server.close();
});
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"regexp"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"
	"time"
)

// ===========================================
// 1. SNOWFLAKE ID GENERATOR
// ===========================================
type SnowflakeGenerator struct {
	mu            sync.Mutex
	epoch         int64
	workerID      int64
	sequence      int64
	lastTimestamp  int64
	workerIDBits  uint
	sequenceBits  uint
	maxSequence   int64
	workerIDShift uint
	timestampShift uint
}

func NewSnowflakeGenerator(workerID int64) *SnowflakeGenerator {
	const workerIDBits uint = 10
	const sequenceBits uint = 12
	return &SnowflakeGenerator{
		epoch:          1700000000000,
		workerID:       workerID,
		workerIDBits:   workerIDBits,
		sequenceBits:   sequenceBits,
		maxSequence:    (1 << sequenceBits) - 1,
		workerIDShift:  sequenceBits,
		timestampShift: sequenceBits + workerIDBits,
	}
}

func (s *SnowflakeGenerator) Generate() int64 {
	s.mu.Lock()
	defer s.mu.Unlock()

	timestamp := time.Now().UnixMilli() - s.epoch

	if timestamp == s.lastTimestamp {
		s.sequence = (s.sequence + 1) & s.maxSequence
		if s.sequence == 0 {
			for timestamp <= s.lastTimestamp {
				timestamp = time.Now().UnixMilli() - s.epoch
			}
		}
	} else {
		s.sequence = 0
	}

	s.lastTimestamp = timestamp

	return (timestamp << int64(s.timestampShift)) |
		(s.workerID << int64(s.workerIDShift)) |
		s.sequence
}

// ===========================================
// 2. BASE62 ENCODER
// ===========================================
const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

func base62Encode(num int64) string {
	if num == 0 {
		return string(base62Chars[0])
	}
	var result strings.Builder
	n := uint64(num) // treat as unsigned for encoding
	for n > 0 {
		result.WriteByte(base62Chars[n%62])
		n /= 62
	}
	// Reverse
	runes := []rune(result.String())
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	return string(runes)
}

// ===========================================
// 3. DATA TYPES
// ===========================================
type URLRecord struct {
	ID          int64     `json:"id"`
	ShortCode   string    `json:"shortCode"`
	OriginalURL string    `json:"originalUrl"`
	UserID      string    `json:"userId"`
	CreatedAt   time.Time `json:"createdAt"`
	ExpiresAt   *time.Time `json:"expiresAt,omitempty"`
	ClickCount  atomic.Int64 `json:"-"`
	Clicks      int64     `json:"clickCount"`
}

type ClickEvent struct {
	ShortCode string `json:"shortCode"`
	Timestamp string `json:"timestamp"`
	IP        string `json:"ip"`
	UserAgent string `json:"userAgent"`
	Referer   string `json:"referer"`
}

// ===========================================
// 4. URL SHORTENER SERVICE
// ===========================================
type URLShortener struct {
	mu        sync.RWMutex
	idGen     *SnowflakeGenerator
	urls      map[string]*URLRecord // short_code -> record
	analytics chan ClickEvent
	baseURL   string
}

func NewURLShortener(workerID int64, baseURL string) *URLShortener {
	s := &URLShortener{
		idGen:     NewSnowflakeGenerator(workerID),
		urls:      make(map[string]*URLRecord),
		analytics: make(chan ClickEvent, 10000),
		baseURL:   baseURL,
	}
	go s.processAnalytics()
	return s
}

func (s *URLShortener) CreateShortURL(originalURL, userID string, expiresIn *int) (string, string, error) {
	if _, err := url.ParseRequestURI(originalURL); err != nil {
		return "", "", fmt.Errorf("invalid URL format")
	}

	// Deduplication check
	s.mu.RLock()
	for _, r := range s.urls {
		if r.OriginalURL == originalURL && r.UserID == userID {
			s.mu.RUnlock()
			return fmt.Sprintf("%s/%s", s.baseURL, r.ShortCode), r.ShortCode, nil
		}
	}
	s.mu.RUnlock()

	id := s.idGen.Generate()
	shortCode := base62Encode(id)

	record := &URLRecord{
		ID:          id,
		ShortCode:   shortCode,
		OriginalURL: originalURL,
		UserID:      userID,
		CreatedAt:   time.Now().UTC(),
	}
	if expiresIn != nil {
		t := time.Now().Add(time.Duration(*expiresIn) * time.Second)
		record.ExpiresAt = &t
	}

	s.mu.Lock()
	s.urls[shortCode] = record
	s.mu.Unlock()

	return fmt.Sprintf("%s/%s", s.baseURL, shortCode), shortCode, nil
}

func (s *URLShortener) Resolve(shortCode, ip, userAgent, referer string) (string, error) {
	s.mu.RLock()
	record, exists := s.urls[shortCode]
	s.mu.RUnlock()

	if !exists {
		return "", fmt.Errorf("not found")
	}

	if record.ExpiresAt != nil && time.Now().After(*record.ExpiresAt) {
		return "", fmt.Errorf("expired")
	}

	record.ClickCount.Add(1)

	// Async analytics
	select {
	case s.analytics <- ClickEvent{
		ShortCode: shortCode,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
		IP:        ip,
		UserAgent: userAgent,
		Referer:   referer,
	}:
	default:
		// Buffer full, drop event (in production: log warning)
	}

	return record.OriginalURL, nil
}

func (s *URLShortener) GetStats(shortCode string) (*URLRecord, error) {
	s.mu.RLock()
	record, exists := s.urls[shortCode]
	s.mu.RUnlock()
	if !exists {
		return nil, fmt.Errorf("not found")
	}
	record.Clicks = record.ClickCount.Load()
	return record, nil
}

func (s *URLShortener) processAnalytics() {
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	var buffer []ClickEvent

	for {
		select {
		case event := <-s.analytics:
			buffer = append(buffer, event)
		case <-ticker.C:
			if len(buffer) > 0 {
				log.Printf("[Analytics] Flushed %d click events", len(buffer))
				buffer = buffer[:0]
			}
		}
	}
}

// ===========================================
// 5. RATE LIMITER
// ===========================================
type RateLimiter struct {
	mu      sync.Mutex
	windows map[string][]int64
}

func NewRateLimiter() *RateLimiter {
	return &RateLimiter{windows: make(map[string][]int64)}
}

func (rl *RateLimiter) IsAllowed(key string, maxReq int, windowMs int64) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now().UnixMilli()
	timestamps := rl.windows[key]

	var valid []int64
	for _, t := range timestamps {
		if t > now-windowMs {
			valid = append(valid, t)
		}
	}
	valid = append(valid, now)
	rl.windows[key] = valid

	return len(valid) <= maxReq
}

// ===========================================
// 6. HTTP SERVER
// ===========================================
func main() {
	baseURL := os.Getenv("BASE_URL")
	if baseURL == "" {
		baseURL = "http://localhost:3000"
	}
	svc := NewURLShortener(1, baseURL)
	rl := NewRateLimiter()

	codePattern := regexp.MustCompile(`^/([a-zA-Z0-9]+)$`)
	statsPattern := regexp.MustCompile(`^/api/stats/([a-zA-Z0-9]+)$`)

	mux := http.NewServeMux()

	mux.HandleFunc("/api/shorten", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			writeJSON(w, 405, map[string]string{"error": "Method not allowed"})
			return
		}

		ip := r.RemoteAddr
		if !rl.IsAllowed(ip, 10, 60000) {
			writeJSON(w, 429, map[string]string{"error": "Too many requests"})
			return
		}

		var body struct {
			URL       string `json:"url"`
			UserID    string `json:"userId"`
			ExpiresIn *int   `json:"expiresIn"`
		}
		if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&body); err != nil {
			writeJSON(w, 400, map[string]string{"error": "Invalid JSON"})
			return
		}
		if body.URL == "" {
			writeJSON(w, 400, map[string]string{"error": "url is required"})
			return
		}

		userID := body.UserID
		if userID == "" {
			userID = "anonymous"
		}

		shortURL, shortCode, err := svc.CreateShortURL(body.URL, userID, body.ExpiresIn)
		if err != nil {
			writeJSON(w, 400, map[string]string{"error": err.Error()})
			return
		}

		writeJSON(w, 201, map[string]string{"shortUrl": shortURL, "shortCode": shortCode})
	})

	mux.HandleFunc("/api/stats/", func(w http.ResponseWriter, r *http.Request) {
		m := statsPattern.FindStringSubmatch(r.URL.Path)
		if m == nil {
			writeJSON(w, 404, map[string]string{"error": "Not found"})
			return
		}
		stats, err := svc.GetStats(m[1])
		if err != nil {
			writeJSON(w, 404, map[string]string{"error": "URL not found"})
			return
		}
		writeJSON(w, 200, stats)
	})

	mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(w, 200, map[string]string{"status": "ok"})
	})

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		m := codePattern.FindStringSubmatch(r.URL.Path)
		if m == nil {
			writeJSON(w, 404, map[string]string{"error": "Not found"})
			return
		}

		originalURL, err := svc.Resolve(m[1], r.RemoteAddr,
			r.Header.Get("User-Agent"), r.Header.Get("Referer"))
		if err != nil {
			writeJSON(w, 404, map[string]string{"error": "URL not found or expired"})
			return
		}

		w.Header().Set("Location", originalURL)
		w.Header().Set("Cache-Control", "private, max-age=90")
		w.WriteHeader(http.StatusMovedPermanently)
	})

	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}

	srv := &http.Server{
		Addr:         ":" + port,
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	go func() {
		log.Printf("URL Shortener on http://localhost:%s", port)
		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Shutting down...")
	srv.Close()
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

ডিজাইন সিদ্ধান্তের ব্যাখ্যা

UUID বা auto-increment-এর বদলে Snowflake ID কেন?

  • Auto-increment অনুমানযোগ্য (users URL guess করতে পারে) এবং একাধিক server-এ কাজ করে না
  • UUID 36 character — একটা short URL-এর জন্য বড্ড লম্বা
  • Snowflake ID 64-bit, সময় অনুযায়ী sortable, worker-জুড়ে unique, এবং 7-11 base62 character-এ encode হয়

302-এর বদলে 301 redirect কেন?

  • 301 (Permanent) — browser এটা cache করে, server load কমায়। যেসব link বদলাবে না তার জন্য ব্যবহার করুন।
  • 302 (Temporary) — প্রতিটি click আপনার server-এ আসে। accurate click tracking দরকার হলে ব্যবহার করুন।
  • আমরা caching আর analytics accuracy-র মধ্যে ভারসাম্য রাখতে ছোট max-age সহ 301 ব্যবহার করি।

async analytics কেন?

Click tracking কখনো redirect ধীর করা উচিত নয়। আমরা event buffer করে batch-এ একটি analytics pipeline-এ (Kafka, SQS) flush করি। একটি analytics event হারিয়ে গেলে চলবে; একটি ধীর redirect চলবে না।

মূল কথা

  • Snowflake ID আপনাকে server-জুড়ে কোনো coordination ছাড়াই globally unique, সময় অনুযায়ী sorted, compact ID দেয়
  • Base62 encoding ছোট, URL-safe string বানায় — 7 character দিয়ে 3.5 trillion URL encode করা যায়
  • Hot URL-এর জন্য LRU cache ব্যবহার করুন — উপরের 20% URL-ই 80% traffic পায়
  • Analytics অবশ্যই async হতে হবে — tracking-এর জন্য কখনো redirect path block করবেন না
  • API layer-এ rate limiting অপব্যবহার আটকায় এবং downstream service রক্ষা করে
  • URL deduplication storage বাঁচায় এবং consistency নিশ্চিত করে

বাস্তব ব্যবহার

  • Bitly এই একই architecture ব্যবহার করে মাসে 600M+ click প্রসেস করে
  • TinyURL একটি distributed database-এ কোটি কোটি URL mapping store করে
  • Twitter-এর t.co tracking আর link safety-র জন্য tweet-এর প্রতিটি URL shorten করে
  • YouTube video URL-এর জন্য base64-encoded ID ব্যবহার করে (11 character = 73 quintillion সম্ভাব্য ID)
  • এই architecture মাঝারি hardware-এ প্রতি সেকেন্ডে 100K+ redirect সামলায়। আরও বেশির জন্য Redis caching আর horizontal scaling যোগ করুন।