Skip to content

URL Shortener#

Problem statement (interviewer prompt)

Design a service like bit.ly: shorten a long URL into a 6-8 character code, redirect on access, and report basic click analytics. Support custom aliases, expiry, and 100M new URLs/day with 100:1 read:write - at <100ms redirect latency.

flowchart LR
  U([User])
  W([Web / API])
  ID([ID Generator])
  KV[(Key-Value Store<br/>short -> long)]
  C[(Cache)]
  U -->|POST /shorten| W
  W --> ID
  W --> KV
  U -->|GET /:code| W
  W --> C
  C -. miss .-> KV
  W -- 301/302 --> U

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class U,W client;
    class KV,C datastore;
    class ID compute;
flowchart TB
  subgraph Client[Clients]
    BR([Browser])
    APP([Mobile / API consumer])
  end

  subgraph Edge
    DNS[DNS<br/>tinyurl.com]
    CDN[CDN<br/>redirect-edge cache]
    LB[L7 LB]
    WAF[WAF / Rate Limit]
  end

  subgraph App[App Tier]
    GW[API Gateway]
    WRITE[Shorten Service<br/>POST /shorten]
    READ[Redirect Service<br/>GET /:code]
    ANL[Analytics Service]
  end

  subgraph ID[ID Generation]
    SNOW[Snowflake / Counter<br/>chunked from ZK/etcd]
    B62[Base62 encode<br/>7 chars = 62^7 ≈ 3.5T]
    CUST[Custom alias check<br/>collision check]
  end

  subgraph Data[Data Layer]
    RDS[(Redis hot cache<br/>code -> url<br/>LRU + TTL)]
    BF[Bloom filter<br/>existing codes]
    KV[(Sharded KV<br/>Cassandra / DynamoDB<br/>by hash code)]
    META[(Postgres<br/>users, custom domains, plans)]
    CLICKS[(Click events<br/>Kafka -> ClickHouse)]
  end

  subgraph Async
    Q[[Kafka click topic]]
    AGG[[Stream aggregator<br/>Flink / Spark]]
    DASH[Dashboard]
  end

  subgraph Obs[Observability]
    M[Metrics: redirect QPS,<br/>p99 latency]
    L[Logs]
    T[Traces]
  end

  BR --> DNS --> CDN --> LB --> WAF --> GW
  APP --> DNS
  GW --> WRITE
  GW --> READ
  WRITE --> CUST
  WRITE --> SNOW --> B62
  B62 --> KV
  WRITE --> META
  READ --> RDS
  RDS -. miss .-> KV
  KV -. fill .-> RDS
  READ --> BF
  READ -.fire&forget.-> Q
  Q --> AGG --> CLICKS --> DASH
  App -.metrics.-> M
  App -.logs.-> L
  App -.traces.-> T

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class BR,APP client;
    class DNS,CDN,LB,WAF,GW edge;
    class WRITE,READ,ANL,B62,CUST,BF service;
    class SNOW,KV,META,CLICKS datastore;
    class RDS cache;
    class Q,AGG queue;
    class DASH,M,L,T obs;

Key design choices#

  • ID strategy: pre-generate counter blocks from ZooKeeper/etcd, encode base62, 6-8 chars.
  • Alternative: hash(longUrl) → base62 → check collision (deterministic, dedupes).
  • Storage: Cassandra/DynamoDB keyed by code, 2 replicas across AZs.
  • Redirect: 301 (cacheable, browser caches forever) vs 302 (every hit logged).
  • Analytics: never block redirect on logging - async to Kafka.

API#

POST /shorten { url, custom_alias?, ttl? } -> { code, short_url }
GET  /:code              -> 301/302 to long url
GET  /api/v1/:code/stats -> { clicks, geo, referers, time series }

Schema#

url(code PK, long_url, owner_id, created_at, expires_at, custom bool)
clicks(code, ts, ip_hash, ua, geo, referer)  -- ClickHouse / BQ
users(id, plan, api_key_hash)

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD Load balancer / GSLB L4/L7 traffic distribution and failover load-balancer
HLD CDN edge caching for static assets cdn
HLD API gateway / BFF single ingress, auth, rate limit, routing api-gateway
HLD Pub/Sub & message brokers topics, consumer groups, delivery semantics pub-sub-pattern
HLD Leader/follower replication sync/semi-sync/async replication, failover replication-leader-follower
HLD Probabilistic data structures Bloom, HLL, Count-Min, MinHash, t-digest probabilistic-data-structures
HLD Observability metrics, logs, traces, SLOs observability
HLD Batch & stream processing Lambda vs Kappa, watermarks, windows batch-stream-processing
LLD Behavioural patterns Strategy, Observer, State, Command, Chain behavioral-patterns

Quick reference#

Functional#

  • Shorten arbitrary URL → ≤ 7-char code.
  • Redirect on GET.
  • Optional custom alias.
  • Optional TTL / expiry.
  • Click analytics.

Non-functional#

  • 99.99% availability on redirect.
  • < 100 ms p99 redirect latency.
  • Read:Write ≈ 100:1.

Capacity (back-of-envelope)#

  • 100M new URLs/day → 1,160 writes/s avg, 5k peak.
  • 100:1 reads → 100k reads/s avg, ~500k peak.
  • 5 years × 100M × 500 B ≈ 100 TB storage.
  • Hot working set ~10 GB cache.

ID design math#

  • Base62 with 7 chars: 62^7 ≈ 3.5 × 10^12 codes. Plenty for 100B URLs.
  • Pre-allocate ranges of 100k IDs per app instance via ZooKeeper counter (no per-request coord).

API#

POST /v1/shorten        body={url, alias?, ttl_days?}
GET  /{code}            -> 301 redirect
GET  /v1/{code}         -> { url, created_at, clicks }
DELETE /v1/{code}       (owner only)

Data model#

  • url_map(code PK, long_url, owner, expires_at) - Cassandra / DynamoDB.
  • clicks - ClickHouse (event store).
  • meta - Postgres (users, plans, billing).

Trade-offs#

  • 301 vs 302: 301 cached → fewer hits, worse analytics. Most use 302.
  • Counter-based code is short & sequential (enumerable); use base62 with random salt or hash-based to avoid scraping.
  • Hash-based dedup prevents duplicate shortens of same URL but complicates per-user codes.
  • Eventually consistent stats acceptable; redirect must be strongly consistent (or read-your-write within session).

Refs#

  • bit.ly engineering blog, TinyURL/Bitly architecture talks, ByteByteGo URL shortener video, Grokking SDI.

FAQ#

How do you generate short codes?#

Either hash the URL and take a base62 prefix with collision retry, or assign a monotonically increasing counter from an ID service and base62 encode the integer.

Should the redirect be 301 or 302?#

Use 302 to keep analytics flowing through your service on every click. Use 301 if SEO and browser caching matter more than tracking.

What database fits a URL shortener?#

A key-value store like DynamoDB, Cassandra, or Redis backed by an OLTP database fits the 100:1 read ratio, with the short code as the partition key.

How do custom aliases work without collisions?#

Treat the alias namespace as reserved and check the KV store before inserting. Generated codes use a distinct prefix or length class so they never collide with custom ones.

How do you handle 100M new URLs per day?#

Estimate 1200 writes per second average and 10x peak. Shard the KV store by code, front reads with a CDN, and use a 64-bit ID generator on the write path.

  • Consistent Hashing: distributes short-code lookups across database shards with minimal hotspots
  • Caching Strategies: read-aside caching of hot short-code mappings is the primary latency optimization
  • Database Sharding: horizontal sharding of the mapping table handles billions of short links

Further reading#

Curated, high-credibility sources for going deeper on this topic.

Video walkthrough

Beginner System Design Interview: Design Bitly : via Hello Interview