System Design Library

URL Shortener (TinyURL)

Map billions of long URLs to short codes and redirect in <50ms.

Open the interactive version → diagrams, practice & more

Requirements

Functional

  • Create short code for a URL
  • Redirect code → URL
  • Optional custom alias & expiry
  • Click analytics

Non-functional

  • p99 redirect <50ms
  • High availability
  • Read:write ~100:1

Scale

~100M new links/day, billions of redirects/day

The approach

Tiny write path (generate unique code, store mapping) + a heavily-cached read path. Reads go App → Cache → DB; the cache absorbs the read skew. Analytics fire-and-forget to a queue.

Key components

Client → App → Cache → KV/SQL store · Queue + workers for analytics · optional CDN

Numbers that matter

Senior deep-dive

Code generation is the crux — everything else is a cached key-value lookup.

A global counter → base62 is dense but needs coordination (hand out ID ranges per host, or use Snowflake-style IDs); hashing risks collisions (retry on conflict).

Keep analytics off the redirect path — it is the hot path and must stay <50ms.

ID generation: the only hard part

Auto-increment is the trap — a single sequence is a write bottleneck and leaks your volume. Hand out ID ranges to each app host (grab a block, allocate locally) or use Snowflake-style IDs (time + machine + counter), then base62-encode. Hashing (MD5 → truncate) works but collides, so you must check-and-retry — an extra read on every write.

The read path is a cache problem

A redirect is a single-key lookup, so cache it hard — App → Cache → DB, the cache absorbing the 100:1 skew. Hot links live in memory; the tail of rarely-clicked links is what misses and hits the DB. Size the cache to your hot set, and push popular mappings to the CDN edge so most redirects never reach the origin.

Custom aliases and the uniqueness check

User-chosen aliases need a real uniqueness guarantee — a conditional insert or unique constraint, never read-then-write (it races). Keep a separate keyspace for custom vs generated codes so they can't collide, and validate length, charset, and profanity up front.

Analytics without slowing redirects

Never log clicks synchronously on the redirect — emit an event to a queue and aggregate asynchronously. Losing a click is fine; adding latency to every redirect is not. At scale, approximate (HyperLogLog) or pre-aggregate by time bucket instead of counting rows.

Expiry, deletion, and storage

Expiry is a TTL checked on read plus a lazy background sweep — don't eagerly scan billions of rows. The store is a simple KV (DynamoDB/Cassandra) or sharded SQL keyed by code: no joins, single-row reads, so it shards cleanly by code hash.

What breaks at scale

At billions of redirects/day the bottlenecks are ID-allocation coordination, cache hit-rate, and DB read fan-out — not storage (the data is tiny). Geo-replicate reads (a mapping is immutable once created), and watch the unpopular-link tail that defeats caching. Writes are easy; the read scale and global ID uniqueness earn the senior signal.

In production

Bit.ly, TinyURL, and t.co all run this shape: a tiny coordinated-ID write path and a massively-cached read path, often with the mapping served from the CDN edge. The interesting engineering is never the shortening — it is ID allocation without a global bottleneck, and keeping the redirect hot path fast at billions/day.

Common mistakes

Related System Design Library

Part of System Design Library on SystemLore — system design interview prep with 148 deep topics, interactive diagrams, and a practice game. Practice this one →