A URL shortener takes a long URL and returns a short, unique alias. When a user visits the short link, the service looks up the original URL and redirects them. It sounds simple, but it's a great interview problem because it touches unique ID generation, read-heavy scaling, caching, and storage trade-offs.
Functional Requirements
The core features we need to support:
- Shorten a URL — given a long URL, return a short alias (e.g.
https://gdt.io/aZ8kP1). - Redirect — given a short alias, redirect to the original long URL.
- Custom aliases (optional) — let users pick their own alias when available.
- Expiration (optional) — links can expire after a configurable time.
Out of scope for a first pass: user accounts, analytics dashboards, and link editing. Call these out so the interviewer knows you're scoping deliberately.
Scale Requirements
Always anchor the design with rough numbers. A reasonable set of assumptions:
- 100M new URLs created per month → ~40 writes/sec on average.
- Read:write ratio of 100:1 → ~4,000 reads/sec, this is a read-heavy system.
- 5 years of retention → 100M × 12 × 5 = 6B URLs total.
- Each record ~500 bytes → 6B × 500B ≈ 3 TB of storage.
The big takeaways: reads dominate (cache aggressively) and storage is modest enough for a single sharded database.
Non-Functional Requirements
We pull these from adjectives in the problem statement:
- High availability — redirection must work 24/7; a dead short link is a broken promise.
- Low latency — redirects should feel instant (single-digit milliseconds).
- High durability — once created, a short URL must never be lost.
- Uniqueness — every short code maps to exactly one long URL, no collisions.
- Scalability — handle growth in both stored links and read traffic.
Availability is favored over strong consistency here: it's fine if a freshly created link takes a moment to propagate, but it's not fine if redirects go down.
Data Model
A single table is enough for the core service:
| Field | Type | Notes |
|---|---|---|
short_code | string (PK) | 7-char base62 alias |
long_url | string | the original URL |
created_at | timestamp | for retention / expiration |
expires_at | timestamp (nullable) | optional TTL |
We index on short_code (the primary lookup). Analytics (click counts) are best kept in a separate store so high-volume write traffic doesn't slow down redirects.
API Endpoints
Two endpoints cover the core flows:
Use a 302 (temporary) redirect rather than 301 (permanent) so the browser keeps hitting our service — that preserves our ability to expire links and (later) count clicks.
High-Level Design
The request flow:
- Write path — client calls
POST /api/shorten; the API server generates a unique ID, encodes it to base62, persists{short_code, long_url}, and returns the short URL. - Read path — client requests
/{short_code}; the API server checks the cache first, falls back to the database, then issues a 302 redirect.
Because reads outnumber writes 100:1, a cache (e.g. Redis) in front of the database absorbs the vast majority of traffic.
Deep Dive: Generating Unique Short Codes
The heart of the problem. We need codes that are unique, short, and hard to guess in sequence. Options:
- Hashing (MD5/SHA + truncate) — hash the long URL and take the first few characters. Simple, but truncation causes collisions that you must detect and retry.
- UUID — guaranteed unique but 128 bits → far too long for a "short" URL.
- Auto-increment counter + base62 encode (recommended) — a global counter yields a unique integer; encode it to base62 (
[a-zA-Z0-9]). With 7 characters, base62 gives 62⁷ ≈ 3.5 trillion combinations — plenty. - Snowflake / distributed ID — for very high write throughput, generate IDs across machines without a single counter bottleneck.
Base62 is preferred over base64 because it avoids URL-unfriendly characters (+, /, =) so codes are safe in links without escaping.
Deep Dive: Scaling the Read Path
Redirects are the hot path. To keep them fast:
- Cache aggressively — keep popular
short_code → long_urlmappings in Redis. A high hit rate means most redirects never touch the database. - Eviction — use LRU; URL access tends to follow a power-law (a few links get most of the traffic).
- Read replicas — on cache miss, read from replicas to spread load off the primary.
- CDN / edge — for globally popular links, redirects can be served close to the user.
Deep Dive: Scaling Storage
At ~3 TB over five years, storage is not the bottleneck, but plan for growth:
- Sharding — partition by
short_code(e.g. consistent hashing) so no single node holds everything. - NoSQL fit — the access pattern is a simple key lookup, so a key-value or wide-column store (DynamoDB, Cassandra) fits naturally and scales horizontally.
- Cleanup — a background job purges expired links to reclaim space.
Wrap-Up & Trade-offs
A solid answer hits these points:
- Counter + base62 for short, collision-free codes.
- Cache-first reads because the system is overwhelmingly read-heavy.
- 302 redirects to retain control over expiration and future analytics.
- Separate analytics store so click tracking never slows the redirect path.
- Favor availability over strong consistency for redirects.
Common follow-ups: how to prevent abuse/spam links, how to handle custom-alias collisions, and how to add per-link analytics without hurting latency.