Design a URL shortener — Bit.ly-style beginner-friendly walkthrough

Design a URL shortener

Junior-friendly Bit.ly walkthrough: requirements, entities, API, create + redirect flows, 301 vs 302, unique short codes (hash vs counter vs batching), caching, read/write split, and what interviewers expect at each level.

Understanding the problem

Bit.ly is a URL shortening service: users paste a long URL, get a short link, and anyone who opens the short link lands on the original. Many products also sell analytics — we'll keep that below the line so you learn the core design first.

Long BookMyShow URL shrinking to a short shareable link.
WhatsApp link diet — same destination, easier to forward, must never break.
01Requirements

Create + redirect · alias/expiry optional.

02API

POST /urls · GET /{code}.

03Codes

Hash vs counter · UNIQUE net.

04Scale

Cache · read/write split.

Pair with Delivery framework, Scaling reads, Caching, Indexing, and Redis.

Functional requirements

Start every interview by pinning what the system must do for users. Interviewers sometimes hand you the list; often you propose it. Zero in on the top 3–4 features — don't get lost in bells and whistles.

Non-functional requirements

NFRs are how the system must operate — latency, availability, scale — not features. Frame them as benchmarks.

Capacity sketch: 1B URLs, 500GB storage, ~1 write/sec, read-heavy.
Whiteboard capacity strip — circle "reads ≫ writes" before drawing boxes.

Core entities

Start with a broad list — not every column. Tell the interviewer you'll refine the schema in high-level design.

ShortURL, User, and ClickAnalytics entities with created_by relationship.
User creates a ShortURL 1:N — ClickAnalytics sits below the line until asked for.
  • Original URL — the long URL the user wants to shorten.
  • Short URL / short code — the compact code (and full short link on your domain).
  • User — who created it (even if auth is out of scope, the entity often appears).
  • ClickAnalytics — below the line, but naming it (clicks, geo, referrer, tied to ShortURL 1:N) shows you scoped it rather than forgot it.

The API

APIs are the contract between client and server. Walk functional requirements one-by-one. In interviews, default to REST and pick the right verb: POST create, GET read, PUT update, DELETE delete.

POST /urls
{
  "long_url": "https://www.example.com/some/very/long/url",
  "custom_alias": "optional_custom_alias",
  "expiration_date": "optional_expiration_date"
}
→
{
  "short_url": "http://short.ly/abc123"
}

We use POST because we create a new mapping row. Optional fields stay optional.

GET /{short_code}
→ HTTP 302 Found
Location: https://www.example.com/some/very/long/url

GET is correct — we're reading an existing mapping. Status code details (301 vs 302, 410 Gone for expired) land in the high-level design.

High-level design — create a short URL

Design one functional requirement at a time. First: users submit a long URL and get a short one.

Whiteboard: Client to Primary Server to Database with Urls schema — Create a short url.
Create a short url — Client ↔ Primary Server ↔ Database. Write: generate code, save. Schema on the right.
  • Client — web or mobile app.
  • Primary server — validation, code generation, business rules.
  • Database (Urls) — short code (or custom alias), original url, creationTime, expirationTime?, createdBy.
  1. Client sends POST /urls with long URL (± custom alias, expiry).
  2. Server validates URL format (simple library / regex is fine in an interview).
  3. Dedup? Optional — return existing code for the same long URL. Most products allow many shorts for one long URL (different expiry, analytics, aliases). Say the trade-off: storage vs product flexibility.
  4. Generate a short code (abstracted for now) — or use the custom alias after uniqueness check. Prefix/namespace generated codes so they don't collide with custom aliases.
  5. Insert mapping; return http://short.ly/{code}.

High-level design — redirect

The short link lives on your domain — e.g. short.ly/abc123 always hits your servers first.

Whiteboard: Client, Primary Server, Database with Write and Read steps and 302 redirect.
Redirect to original url — same spine; Read: look up in DB, return 302. Both POST /urls and GET /{short_code} on the left.
Basic high-level design with Client, Primary Server, Database and Urls fields.
Basic high-level design — draw this first before adding cache or microservices.
  1. Browser GET /abc123.
  2. Server looks up abc123 (cache, then DB).
  3. If found and not expired → return redirect to long URL. If expired → 410 Gone.
  4. Browser follows Location automatically — users barely notice the hop.

301 vs 302

  • 301 Permanent — browsers often cache; later visits may skip your server entirely.
  • 302 Found — typically not cached that aggressively; every click can hit you first.

Cleanup: periodic job to delete expired rows or leave them and check expires_at on read. Set cache TTL ≤ expiration so stale shorts don't live in Redis after they should die.

Deep dive — unique short codes

Constraints: codes must be unique, as short as practical, and cheap to generate.

Six options: hash, counter, custom alias, batching, multi-region ranges, UNIQUE safety net.
Interview move: compare hash vs counter, then show how you scale the counter.

Option A — Hash the long URL

Hash (MD5/SHA), take a prefix, base62-encode. Pros: no central counter. Cons: collisions — on UNIQUE violation, lengthen prefix or retry with salt. Same long URL always hashes the same (dedup-friendly) unless you salt per user.

Hash pipeline: canonicalize, hash, base62, slice to 8 chars; Primary Server with Hash Function box.
Hash Function path — canonicalize → hash → base62 → take 8 chars. Still need UNIQUE + retry on collision.
input_url = "https://www.example.com/some/very/long/url"
canonical_url = canonicalize(input_url)  # lower host, strip default ports
hash_code = hash_function(canonical_url)
short_code = base62_encode(hash_code)[:8]  # 8-character short code

Option B — Global counter + base62 (recommended teaching default)

Atomically increment a counter, convert to base62 (0-9a-zA-Z). Guarantees uniqueness if the counter is correct; codes stay short. Needs coordination when you scale writers.

Option C — Custom alias

User-chosen code after availability check. Keep a separate namespace or reserved prefix so aliases never collide with future counter values.

Deep dive — fast redirects (scale reads)

Without an index, lookup is a full table scan — unusable at millions of rows. With a primary key on short_code, DB lookup is fine for moderate QPS — but at redirect scale you still want a cache.

Whiteboard: Primary Server with random num generator, Cache redis checked before Database.
In-Memory Cache — 1) check cache 2) check DB. Redis key: short_code → value: original_url.
  • Cache TTL aligned with expiry (or shorter).
  • On create/update/expire — invalidate or set the key.
  • Hot viral codes stay in memory — that's the win.
  • Code generation can start as a random/counter box on the server — upgrade to Redis counter when you split writers.

Deep dive — scale to 1B URLs and 100M DAU

Storage napkin: ~200–500 bytes/row × 1B ≈ ~500GB — within one modern Postgres on good disks. Writes ~100k creates/day ≈ ~1/sec average — almost any serious DB works. Pick what you know; Postgres is a fine interview default.

If the DB dies: replication (hot standby) and backups. Mention them; don't boil the ocean.

Final Design: Client, API Gateway, Write Service with Global Counter, Read Service with Redis cache, shared Database and Urls schema.
Final Design — API Gateway routes to Write Service (Global Counter) or Read Service (Redis cache); both share Database.

Split read and write services

Because traffic is asymmetric, separate Write service (POST /urls) from Read service (GET /{code}). An API Gateway routes to the correct microservice. Scale each fleet independently.

The counter problem

Many write instances need one source of truth for the next ID — the Global Counter (Redis INCR). One extra network hop per create is usually fine at ~1 write/sec — but still teach the upgrade:

  1. Counter batching — each writer claims 1000 IDs (INCRBY 1000), uses them locally, then claims again. Fewer Redis RTTs; gaps OK.
  2. HA — Redis Sentinel/Cluster failover. Lost unreplicated counter values are OK if UNIQUE still holds.
  3. Multi-region — allocate disjoint ranges (region A: 0–1B, B: 1B–2B) so regions don't coordinate on every write. Reads served from regional caches.

Cost and performance levers

What interviewers expect by level

URL shortener is often labeled "entry-level," but the bar still rises with seniority.

Architecture by level

Three progressive sketches — beginner spine, mid optimizations, pro production depth. Use the matching diagram in the interview; say the tradeoff out loud.

Beginner URL shortener: client, API, Postgres mapping.
Beginner architecture

Pros: Tiny surface — create + redirect + UNIQUE code is enough to pass an entry screen. Cons: One process + one DB becomes the bottleneck under read-heavy redirects; no shared counter means uniqueness breaks the moment you add a second write replica.

Mid URL shortener: split read/write APIs with Redis cache and counter.
Mid architecture

Pros: Cache absorbs redirect QPS; Redis counter coordinates IDs across write instances; R/W split matches the 100:1 traffic shape. Cons: Cache invalidation / TTL for expiry adds edge cases; you're now operating Redis HA — a failover that replays ID ranges can create duplicate codes if claim isn't durable.

Pro URL shortener: CDN, multi-region ID ranges, HA cache and replicas.
Pro architecture

Pros: Multi-region ID ranges and CDN cut latency worldwide; replicas and failover story show production judgment. Cons: Complexity tax — custom aliases, analytics, and security of predictable codes compete for interview time; over-building multi-region before proving single-region p99 is a common fail.

Sketch of create, redirect, and cache hit paths for interview Q&A.
The sketch every level below draws from — create on top, redirect with the cache branch on the bottom.
Interview takeaway

Entry: working spine. Mid: trade-offs without prompting. Principal: production and evolution. Match depth to the bar.

Wrapping up

A URL shortener teaches the delivery framework end-to-end: clarify requirements, sketch entities and APIs, build a simple HLD, then deep-dive uniqueness, read scaling, and write coordination. Keep analytics and auth below the line until asked.

Related: Delivery framework · Design Dropbox · Scaling reads · Caching · Redis · PostgreSQL · Common patterns.

← Lattice