URL Shortener (bit.ly)
Design a URL shortening service like bit.ly that converts long URLs into short aliases and redirects users.
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.
Reference Documents
Designing a URL Shortener — System Design Primer
Deep-dive into URL shortener LLD including hashing, DB schemas, and caching.
ArticleBuilding a URL Shortener — ByteByteGo
Step-by-step LLD walkthrough with sequence diagrams and DB schema.
BlogBase62 Encoding Explained
Visual explanation of Base62 encoding for short key generation.
VideoLLD 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
- EncodeService — accepts a long URL, generates a short key, writes to DB
- RedirectService — accepts a short key, looks up the long URL (cache-first), returns 302
- IDGeneratorService — issues unique int64 IDs via range allocation (no central lock)
- ClickEventConsumer — drains the analytics queue and batches writes to the data warehouse
Data Model
Database Schema
MySQL — url_mappings
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;
Redis — Hot Mapping Cache
- Key:
url:{shortKey}→ Value:{longUrl}(JSON string) - TTL: 86400s (24h), refreshed on access
- Eviction: allkeys-lru
ClickHouse / DW — click_events
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);
API Design
Service Interface Contracts
EncodeService
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
}
RedirectService
// 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
IDGeneratorService
// 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
}
Sequence Diagrams
Key Request Flows
Shorten URL Flow
Redirect Flow
LLD Tradeoffs
Low-Level Design Decisions
| Decision | Option A | Option B | Chosen |
|---|---|---|---|
| ID Generation | ZooKeeper range allocation | Twitter Snowflake | ZooKeeper ranges — simpler, no clock drift issues |
| Cache Strategy | Write-through (write on create) | Lazy (populate on first read) | Lazy — many URLs never clicked, save RAM |
| Analytics Write | Synchronous DB write per click | Async Kafka → batch | Async Kafka — prevents redirect latency spike |
| Custom Alias Check | DB SELECT on every shorten | Bloom filter + DB | Bloom filter — O(1) probabilistic check avoids DB round-trip on cold alias |
| Expired URL Cleanup | Lazy delete on access | Background TTL sweeper | Both — lazy for reads + nightly background sweep |
Core Classes
Core Class Interfaces
// 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>
}