Skip to content

Walkthrough — Design a URL Shortener (TinyURL / Bitly)

The single most common system design interview question. This walks the full four-step approach from the Introduction.


Step 1 — Requirements, constraints, and assumptions

Clarifying questions

  • Traffic volume? (e.g., "100 million URLs generated per day")
  • How short? (as short as possible)
  • Custom aliases allowed?
  • Expiry / TTL on links?
  • Analytics (click counts, geography) needed?

Functional requirements

  • Shorten: given a long URL, return a unique short alias.
  • Redirect: given a short URL, redirect to the original.
  • (Optional) custom alias, expiration, analytics, user accounts.

Non-functional requirements

  • Highly available and very low-latency redirects.
  • Read-heavy (roughly 100:1 read-to-write).
  • Short codes must be unique and shouldn't collide.

Back-of-the-envelope

Metric Estimate
New URLs / day 100 million
Write rate 100M / 86,400 s ≈ 1,157 writes/sec
Read rate (100:1) ≈ 115,700 reads/sec
Rows over 5 years 100M × 365 × 5 ≈ 182.5 billion
Storage (≈500 B/row) ≈ 91 TB
Cache (hot 20%) ≈ 20M entries in RAM

Step 2 — High-level design

                    ┌──────────────┐
 Client ──────────▶ │   DNS / CDN  │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐       ┌──────────────┐
                    │ Load Balancer│──────▶│  App servers │ (stateless)
                    └──────┬───────┘       └──────┬───────┘
                           │                      │
                    write  │                      │ read
                           ▼                      ▼
                    ┌────────────┐         ┌────────────┐    miss   ┌───────────┐
                    │ Key pool / │         │   Redis    │──────────▶ │ Sharded   │
                    │  generator │         │   (cache)  │            │  SQL DB   │
                    └────────────┘         └────────────┘            └───────────┘

Two distinct paths:

  • Write path: POST /shorten → generate a code → store in DB (and warm cache).
  • Read path: GET /{code} → check cache → on miss, read DB → return 301.

Step 3 — Core components

3.1 Short-code generation

Three common approaches, each with trade-offs:

Approach How Pros Cons
Hash + Base62 MD5/SHA of URL, take first 7 chars, encode Base62 No coordination Collisions (mitigate by retry / appending)
Base62 of a counter Encode a unique 64-bit ID (Snowflake) to Base62 Collision-free, short Needs a distributed ID generator
Pre-generated key pool Offline service generates keys, hands them out Fast writes, no in-line collision Key pool can exhaust; more moving parts

Base62 alphabet: [a-z A-Z 0-9] = 62 chars. A 7-char code gives 62^7 ≈ 3.5 trillion combinations.

Collision handling: use a unique DB constraint; on collision, retry with a different salt/counter. This is why the counter approach is usually preferred — it can't collide.

3.2 Data model (SQL is a natural fit)

CREATE TABLE urls (
  short_code  VARCHAR(10)  PRIMARY KEY,   -- indexed, unique
  long_url    TEXT         NOT NULL,
  created_at  TIMESTAMP    NOT NULL,
  expires_at  TIMESTAMP,                  -- optional
  user_id     BIGINT                       -- optional
);

Why SQL here: structured data, strict schema, need for uniqueness and fast indexed lookups.

3.3 Redirect path

  1. Receive GET /{short_code}.
  2. Look up short_code (cache first, then DB).
  3. Return 301 (permanent — cacheable) or 302 (temporary — if you want to count every click for analytics).

3.4 API

POST /shorten
{ "long_url": "...", "custom_alias": "optional", "expiration": "optional" }
→ { "short_url": "https://short.ly/abc123" }

GET /{short_code}
→ 301 Location: https://original.example.com/very/long/url

Step 4 — Scale the design

Caching (read path is hot)

  • Use cache-aside with Redis: cache short_code → long_url.
  • The 80/20 rule: a small set of URLs drive most traffic — cache them.
  • TTL for freshness; evict when a URL expires or changes.

Sharding (write volume grows)

  • Shard the urls table by short_code (e.g., first 2–3 chars) or via consistent hashing on the code.

  • A distributed ID generator (Snowflake) gives collision-free, roughly sortable IDs to encode as Base62 codes.

Replication & availability

  • Master-slave replication to serve the read-heavy load from replicas.
  • Stateless app servers behind a load balancer for horizontal scaling.

Hardening


Key trade-offs to mention

  • Hash vs counter: collision risk vs the need for a coordinated ID generator.
  • 301 vs 302: cacheability vs per-click analytics.
  • SQL vs NoSQL: uniqueness + indexed lookups favor SQL; a KV store also works for the pure redirect path.

  • Cache vs DB: cache absorbs the hot read load but introduces staleness and invalidation complexity.