mdashikjs/blog
All posts
System Design Interview: Designing a URL Shortener, Step by Step
System Design

System Design Interview: Designing a URL Shortener, Step by Step

System Design6 min

System Design Interview: Designing a URL Shortener, Step by Step

This is the canonical warm-up question, and it is still the one I see candidates botch most often. Here is the full 45-minute walkthrough the way I would deliver it today.

System DesignInterviewArchitecture
Share:

Minute 0–5: scope the problem

Do not start drawing boxes. Ask questions. The interviewer is watching to see if you can narrow down requirements.

  • Is this for public use (Bit.ly) or internal (Slack link unfurls)?
  • Do URLs expire? Custom aliases?
  • What is the read:write ratio? (For shorteners: often 100:1 reads to writes.)
  • Do we need analytics?
  • Which regions?

State your assumptions aloud: "I will assume public, 100M new URLs per month, 10B reads per month, no expiry, optional custom aliases, global."

Minute 5–10: back-of-envelope

  • 100M writes/month ≈ 40 writes/sec average, ~200/sec peak.
  • 10B reads/month ≈ 4k reads/sec average, ~20k/sec peak.
  • Storage: if each record is ~500 bytes, 100M/month × 12 months × 10 years = 12B records × 500B = 6TB. Fits comfortably in a sharded database.
  • Short code length: base62 gives 62^7 ≈ 3.5 trillion codes in 7 characters. Plenty.

Minute 10–15: API and data model

POST /shorten { long_url, custom_alias? } -> { short_url }
GET  /:code -> 302 redirect

Data model (one table):

urls
  code (PK, varchar 10)
  long_url (text)
  created_at
  user_id (nullable)

Indexes: primary on code. That is the only lookup path. Simple.

Minute 15–25: the code generation strategy

This is where candidates reveal themselves.

Option A: hash the URL. Take md5(long_url) and base62-encode the first 7 chars. Problem: collisions. Two different URLs can hash to the same prefix. You need to check-then-insert, which does not work under concurrency without locks.

Option B: random generation with collision retry. Generate 7 random base62 chars, attempt insert, on conflict retry. Works but gets worse as space fills.

Option C: counter + base62 encode. A distributed counter service (e.g., ZooKeeper, or a dedicated Postgres sequence partitioned across shards) hands out batches of IDs. Each ID is base62-encoded to produce the code. No collisions, ever. This is the approach I would advocate.

Discuss the trade-off: counters are predictable (people can enumerate your URLs). If that matters, XOR the counter with a secret before encoding. Bit.ly-class systems care; internal shorteners usually do not.

Minute 25–35: read path and caching

20k reads/sec is well within Redis territory. Put a cache in front of the DB:

  1. GET :code → Redis lookup.
  2. Hit: return the long URL, return 302.
  3. Miss: hit Postgres, populate Redis with a TTL (say 24h), return 302.

With a hit rate of 95%+ (common for URL shorteners due to hot tails), the DB sees ~1k reads/sec. Trivial load.

Mention CDN for the actual 302 response if latency is a concern globally. Many shorteners run the redirect at the edge.

Minute 35–40: write path and consistency

Writes are cheap in this system. 200 writes/sec hits a single well-tuned Postgres happily. Sharding can wait until much later.

Acknowledge replication lag: if a user creates a URL and clicks it immediately, do we guarantee they can? Primary-read for the creating user's session, replica-read for everyone else.

Minute 40–45: scale and failure modes

  • Sharding: shard by code's hash when a single DB is full. All lookups have the code already, so routing is trivial.
  • Analytics: async log to Kafka, aggregate in a data warehouse. Do not write click counts back to the main DB — hot rows will kill you.
  • Cache eviction: hot URLs stay in cache; long tail evicts. Monitor hit rate.
  • Abuse: rate-limit POST /shorten, scan for known-malicious destinations.

The evaluation

Interviewers want to see: clear scoping, honest estimation, sane data model, articulated trade-offs on ID generation, and a plan for reads that matches the read-heavy workload. Get those right and you pass regardless of which specific database you picked.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.