Design a URL Shortener

easy
hashing
databases
caching
scaling

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:

FieldTypeNotes
short_codestring (PK)7-char base62 alias
long_urlstringthe original URL
created_attimestampfor retention / expiration
expires_attimestamp (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:

POST /api/shorten
{ "long_url": "https://example.com/very/long/path", "custom_alias": "optional" }
-> 200 { "short_url": "https://gdt.io/aZ8kP1" }
GET /{short_code}
-> 302 Found
   Location: https://example.com/very/long/path

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:

  1. 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.
  2. 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_url mappings 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.