Skip to content
MetaStack
All cards
Classic designsdesigndifficulty 1/3#url-shortener

Design a URL shortener like bit.ly: create short links, redirect quickly at scale, and track click counts.

Key points

  1. Clarifies scale (reads dominate writes 100:1), link lifetime and whether custom aliases are neededIn plain words: Ask first how busy it is, how long links live and whether people choose their own names, noting far more people click links than create them
  2. Chooses a 7-character base62 code generated from a unique id or random with collision checkIn plain words: Use a seven-character code of letters and digits, made from a unique number or picked at random and checked for clashes
  3. Serves redirects from a cache in front of a key-value or relational store, with 301 vs 302 decided by analytics needsIn plain words: Answer clicks from fast memory in front of a simple store, and pick a permanent or temporary redirect based on whether you need to count clicks
  4. Uses a counter service or async event stream for click analytics rather than a synchronous write on every redirectIn plain words: Count clicks on the side through a stream or counter, so the redirect never waits on a write
  5. Discusses hot links, expiry and abuse (malicious URLs, enumeration of sequential ids)In plain words: Talk about links that go viral, links that expire, and misuse such as harmful targets or guessing codes that go up in order

Stages

  1. 1Requirements

    • Functional, shorten a URL, redirect, optional custom alias, optional expiry, click analytics
    • Non-functional, redirect latency in the tens of milliseconds, very high availability, codes must not be guessable
  2. 2Estimates

    • ~100M new links per month → ~40 writes/s, 100:1 reads → ~4,000 reads/s, peak 10x
    • ~500 bytes per link → ~6 TB over 10 years
  3. 3API

    • POST /links with longUrl, optional alias and expiry, returns shortUrl
    • GET /{code} returns 301 or 302 to the long URL, 404 if unknown or expired
  4. 4Data model

    • links(code PK, long_url, user_id, created_at, expires_at)
    • clicks aggregated per code per day, not one row per click
  5. 5High-level design

    • Stateless API servers behind a load balancer, cache (Redis) for code → URL, key-value or relational store behind it
    • Separate id-generation or code-allocation service
  6. 6Deep dives

    • Code generation, base62 of a unique id vs random with retry vs pre-generated key pool
    • 301 (cacheable, fewer hits, less analytics) vs 302 (every click hits you, full analytics)
    • Analytics via async events to a stream and periodic aggregation
  7. 7Bottlenecks and failure

    • Hot link storms handled by cache and CDN
    • Sequential ids leak creation volume, mitigate with random offset or encryption
    • Malware scanning and rate limiting on creation

Model answer

Requirements

Functional: create a short link for a long URL, redirect when the short link is visited, optional custom alias, optional expiry, click counts per link. Non-functional: redirects must be fast (they sit in front of every page view the link drives) and highly available; codes should not be guessable; creation can be slightly slower.

Estimates

100 million links per month is ~40 writes/s; a 100:1 ratio gives ~4,000 reads/s, peak ~40,000. Ten years of links at ~500 bytes is ~6 TB. Small. The interesting constraints are latency and availability, not throughput.

API

POST /api/links        { longUrl, alias?, expiresAt? } -> { code, shortUrl }
GET  /{code}           -> 302 Location: longUrl   (404 if unknown/expired)
GET  /api/links/{code}/stats -> { clicks, byDay[] }

Data model

links(code PK, long_url, owner_id, created_at, expires_at) and link_clicks_daily(code, day, count). The code is the primary key because every redirect is a point lookup by code.

High-level design

Redirect servers are stateless; the cache holds code → long_url for hot links; the store is a key-value database or a relational database with the code as primary key. Click events are published asynchronously so the redirect path never waits on analytics.

Deep dives

Code generation. Three common schemes:

  1. Base62 of a unique 64-bit id from a Snowflake-style generator: no collisions, but sequential codes let anyone enumerate your links and estimate volume. Mitigate by encrypting or bit-shuffling the id first.
  2. Random 7-character code with a uniqueness check: 62^7 ≈ 3.5 trillion codes, so collisions are rare; retry on conflict.
  3. Pre-generated key pool: a service fills a table with unused random codes; API servers lease batches. Fast and collision-free at the cost of a component to run.

301 vs 302. A 301 (permanent) lets browsers and CDNs cache the redirect, so repeat visits never reach you: lower load, but you lose click counts. A 302 (temporary) sends every click through you, giving full analytics. Most commercial shorteners use 302 for that reason.

Analytics. Never increment a counter row synchronously on each redirect; it serialises writes on hot links. Emit an event (code, timestamp, referrer, user agent) to a stream and aggregate per minute or per day.

Bottlenecks and failure modes

  • A viral link produces millions of redirects per hour: the cache absorbs it, and a CDN can cache 302s briefly too.
  • If the cache tier dies, the database must survive the full read load for a few minutes: size it for that or fail some requests gracefully.
  • Abuse: scan destination URLs against phishing lists, rate-limit creation per user, and block known bad domains.
  • Expiry: lazy-delete on read plus a background sweeper.

Likely follow-ups

  • How would you support custom aliases without breaking uniqueness guarantees?
  • What changes if links must be deleted or edited after creation?

References