WALKTHROUGH · Walkthroughs

Walkthrough: URL Shortener (bit.ly / TinyURL)

Given a long URL, return a short URL (7-char key); optional custom alias.

ArticleWalkthroughs© Arthur Bernier Jr8 steps622 words3 code blocks

1. Requirements#

Functional

  • Given a long URL, return a short URL (7-char key); optional custom alias.
  • Redirect short → long with 301 (permanent, cacheable) or 302 (temporary, lets us count clicks).
  • Optional expiry; basic click analytics.

Non-functional

  • Read-heavy: ~10:1 read:write (or 100:1); redirect p99 < 100 ms.
  • 99.99% availability for redirects; shortening can tolerate brief degradation.
  • Keys unguessable-ish; no collisions; scale to 10 years of data.

2. Estimates#

  • Writes: 100M new URLs/day ÷ 86,400 s ≈ 1,160 writes/s (≈ 1.2k; peak 3x ≈ 3.5k).
  • Reads: 10x → ≈ 11,600 reads/s (peak ≈ 35k).
  • Storage: 100M/day × 365 × 10 yr = 365B records × ~100 bytes (key 7 + URL ~90 + metadata) ≈ 36.5 TB.
  • Key space: base62 (a-z, A-Z, 0-9) with 7 chars = 62^7 ≈ 3.5 × 10^12 — enough for 365B.
  • Bandwidth: reads 11,600/s × ~500 bytes (headers + Location) ≈ 6 MB/s; trivial.
  • Cache: 80/20 rule → 20% of daily reads = 0.2 × 1B × 100 bytes ≈ 20 GB; fits in a Redis cluster.

3. API sketch#

TEXT
POST /api/v1/urls          { longUrl, customAlias?, expiresAt? }  -> 201 { shortUrl, key }GET  /{key}                -> 301/302 Location: longUrl        (404 if missing, 410 if expired)DELETE /api/v1/urls/{key}  (owner only)GET  /api/v1/urls/{key}/stats -> { clicks, byDay... }

Idempotency-Key header on POST; API key auth + rate limit (token bucket at gateway).

4. Data model#

TEXT
urls(  key        VARCHAR(7)  PRIMARY KEY,   -- base62  long_url   TEXT NOT NULL,  user_id    BIGINT NULL,  created_at TIMESTAMPTZ,  expires_at TIMESTAMPTZ NULL)INDEX urls(user_id, created_at)           -- "my links"clicks: append-only events in Kafka -> ClickHouse / Redshift for analytics (not in the hot path)

Store: Postgres sharded by key (hash) or DynamoDB/Cassandra with key as partition key. Hot path is pure key lookup, so a KV store is a natural fit.

5. High-level design#

TEXT
client -> DNS -> CDN/edge (optional cache of 301s) -> L7 LB -> API gateway (auth, rate limit)                                                        |                       +---------------------+----------+------------------+                       v                     v                             v                [Redirect service]    [Shorten service]            [Analytics consumer]                       |                     |                             ^                  Redis cache  <--- fill --- |                             |                       |                     v                             |                  URL store (Postgres shards / DynamoDB) <-- key range from ID generator                       |------------------- click event ---> Kafka --------+

Redirect: check Redis → miss → DB → populate cache → 302 + async click event. Shorten: get unique ID → base62 encode → insert → return.

6. Deep dives#

Key generation

  • Option A: hash (MD5/SHA-256) of long URL, take first 7 base62 chars → collisions need check-and-retry; same URL gives same key (dedupe).
  • Option B: unique numeric ID (Snowflake or DB sequence) → base62 encode → no collisions, no DB check; keys are predictable (mitigate by salting/shuffling bits).
  • Option C: pre-generated Key Generation Service (KGS) that hands out batches of unused random keys from its own DB; app servers cache a block of keys in memory. Pick B or C; say why: "No collision check on the write path."

301 vs 302

  • 301 → browser caches, we lose click data but offload repeat traffic. 302 → every click hits us, we get analytics. Choose 302 if analytics matter; 301 if pure cost minimization.

Caching the redirect

  • Cache-aside in Redis keyed by key, TTL ~24 h, LRU eviction; hit ratio should be high (Zipfian clicks). Negative-cache 404s briefly to blunt scanners. CDN can cache 301s at the edge for the hottest keys.

7. Bottlenecks & scaling#

  • DB reads: cache handles most; add read replicas; KV store scales horizontally by key hash.
  • DB writes: 1-4k/s fits one Postgres primary; shard by key hash when growth demands; consistent hashing if self-managed.
  • Hot keys (viral link): cache absorbs; replicate the key across cache nodes or CDN edge.
  • ID generator as SPOF: multiple Snowflake workers with distinct worker IDs; or KGS with two instances splitting key ranges.
  • Abuse: rate limit per API key/IP; blocklist of malicious domains; CAPTCHA for anonymous shortening.
  • Expiry: lazy delete on read + periodic cleanup job; TTL attribute in DynamoDB.
  • Analytics off the hot path: emit event to Kafka, aggregate in ClickHouse; never count synchronously in the DB.

8. Words to say#

base62, 62^7 ≈ 3.5 trillion, read-heavy 10:1, cache-aside, Redis LRU, 301 vs 302, Snowflake ID, Key Generation Service, collision check, hash prefix, idempotency key, rate limiting at the gateway, hash-based sharding, consistent hashing, hot key, negative caching, CDN edge cache, lazy expiration, Kafka click stream, ClickHouse analytics, 99.99% on redirects.

See also