SystemCraftSystemCraft
Back to Designs

URL Shortener (bit.ly)

Design a URL shortening service like bit.ly that converts long URLs into short aliases and redirects users.

~9 min readeasy difficultyweb
Practice

Low Level Design — covers service internals: DB schemas, API contracts, sequence flows, and class interfaces.

These designs are for learning purposes and may not be 100% accurate or production-ready. Always cross-reference with official docs and other resources. Report an issue if you spot something wrong.

LLD Overview

Low Level Design — URL Shortener

At the LLD level we focus on the internals of each service, their data contracts, and the exact DB schema.

Services to Design

  1. EncodeService — accepts a long URL, generates a short key, writes to DB
  2. RedirectService — accepts a short key, looks up the long URL (cache-first), returns 302
  3. IDGeneratorService — issues unique int64 IDs via range allocation (no central lock)
  4. ClickEventConsumer — drains the analytics queue and batches writes to the data warehouse

Data Model

Database Schema

MySQL — url_mappings

sql
CREATE TABLE url_mappings (
  short_key   VARCHAR(8)   NOT NULL PRIMARY KEY,
  long_url    VARCHAR(2048) NOT NULL,
  user_id     BIGINT UNSIGNED,
  created_at  TIMESTAMP NOT NULL DEFAULT NOW(),
  expires_at  TIMESTAMP,
  click_count BIGINT NOT NULL DEFAULT 0,
  INDEX idx_user    (user_id),
  INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Expand

Redis — Hot Mapping Cache

  • Key: url:{shortKey} → Value: {longUrl} (JSON string)
  • TTL: 86400s (24h), refreshed on access
  • Eviction: allkeys-lru

ClickHouse / DW — click_events

sql
CREATE TABLE click_events (
  short_key    String,
  clicked_at   DateTime,
  ip_hash      String,
  country_code String,
  user_agent   String
) ENGINE = MergeTree()
ORDER BY (short_key, clicked_at);
Expand

API Design

Service Interface Contracts

EncodeService

typescript
interface EncodeRequest {
  longUrl:     string        // required, max 2048 chars
  customAlias?: string       // optional, 4-16 alphanumeric chars
  expiresAt?:  string        // ISO 8601
  userId?:     number
}
interface EncodeResponse {
  shortKey: string           // e.g. "aB3xY9q"
  shortUrl: string           // e.g. "https://bit.ly/aB3xY9q"
  expiresAt?: string
}
Expand

RedirectService

typescript
// GET /{shortKey}
// Returns: HTTP 302 with Location header set to longUrl
// Publishes ClickEvent to queue asynchronously (fire-and-forget)
// Returns HTTP 404 if shortKey not found or expired
Expand

IDGeneratorService

typescript
// Allocates ranges from ZooKeeper: e.g. [5_000_001, 6_000_000]
// Stores current range in memory; requests new range when exhausted
interface IDGeneratorService {
  nextId(): Promise<bigint>   // returns next unique int64
  toBase62(id: bigint): string  // converts to 7-char Base62 string
}
Expand

Sequence Diagrams

Key Request Flows

Shorten URL Flow

Diagram
Expand

Redirect Flow

Diagram
Expand

LLD Tradeoffs

Low-Level Design Decisions

DecisionOption AOption BChosen
ID GenerationZooKeeper range allocationTwitter SnowflakeZooKeeper ranges — simpler, no clock drift issues
Cache StrategyWrite-through (write on create)Lazy (populate on first read)Lazy — many URLs never clicked, save RAM
Analytics WriteSynchronous DB write per clickAsync Kafka → batchAsync Kafka — prevents redirect latency spike
Custom Alias CheckDB SELECT on every shortenBloom filter + DBBloom filter — O(1) probabilistic check avoids DB round-trip on cold alias
Expired URL CleanupLazy delete on accessBackground TTL sweeperBoth — lazy for reads + nightly background sweep
Expand

Core Classes

Core Class Interfaces

typescript
// Shared types
type ShortKey = string  // 7-char Base62

interface URLMapping {
  shortKey:  ShortKey
  longUrl:   string
  userId?:   number
  createdAt: Date
  expiresAt?: Date
}

// EncodeService
class EncodeService {
  constructor(
    private idGen:  IDGeneratorService,
    private db:     URLMappingRepository,
    private cache:  CacheService,
    private bloom:  BloomFilter,
  ) {}

  async shorten(req: EncodeRequest): Promise<EncodeResponse>
  async customShorten(req: EncodeRequest): Promise<EncodeResponse>
  private isExpired(mapping: URLMapping): boolean
}

// URLMappingRepository (DB abstraction)
interface URLMappingRepository {
  insert(mapping: URLMapping): Promise<void>
  findByShortKey(key: ShortKey): Promise<URLMapping | null>
  deleteExpired(): Promise<number>
}

// CacheService (Redis abstraction)
interface CacheService {
  get(key: string): Promise<string | null>
  set(key: string, value: string, ttlSeconds: number): Promise<void>
  delete(key: string): Promise<void>
}
Expand