CodeOath
← All posts
System Design52 min total · 14 parts

System Design Fundamentals for Interviews: Scalability, Trade-offs, and the Framework Interviewers Actually Grade

Part 11 of 14 · ~3 min

Worked Example: Design a URL Shortener

This walks the full five-step framework from chapter 1 against a single concrete problem, start to finish.

Step 1: Clarify Requirements

Functional: given a long URL, return a short one; given a short URL, redirect to the original long URL. Optionally: custom aliases, expiration dates, click analytics. Non-functional, with rough assumed numbers for this walkthrough: 100 million new URLs shortened per month, a 100:1 read-to-write ratio (redirects vastly outnumber new shortenings), and redirects need to feel instantaneous — low double-digit milliseconds.

Step 2: Back-of-the-Envelope Estimation

100M writes/month is roughly 40 writes/second on average. A 100:1 read ratio puts reads at roughly 4,000/second on average — and real traffic isn't flat, so a design should comfortably absorb several times that at peak. Storage: assume each stored record (short code, long URL, metadata) is around 500 bytes; 100M new records/month is 50 GB/month, or about 3 TB after five years — large enough to think about eventually, small enough that it doesn't force sharding on day one.

Step 3: High-Level Design

Client -> Load Balancer -> App Servers (stateless) -> Cache -> Database
                                                    (cache-aside, from ch. 4)

A write (POST /shorten) generates a unique short code, stores {short_code -> long_url}, and returns the short URL. A read (GET /{short_code}) looks up the long URL and responds with an HTTP redirect. Given the 100:1 read-heavy ratio from step 2, a cache in front of the database (cache-aside, exactly as in chapter 4) is close to load-bearing here, not optional — it's absorbing the overwhelming majority of traffic before it ever reaches the database, and its hit rate for popular links directly determines how much database capacity the system actually needs.

Step 4: Deep Dive — Generating the Short Code

This is the interesting part of this specific problem, and it's worth spending the deep-dive budget here rather than on the load balancer or cache, both already covered generically. Two real approaches:

  • Hash-based: hash the long URL (e.g., MD5) and take the first several characters, base62-encoded ([a-zA-Z0-9], 62 characters — 6 characters gives roughly 62⁶ ≈ 56 billion combinations). The problem: truncating a hash creates real collision risk, and two different long URLs can map to the same short code — the system needs a documented resolution strategy (append a character and rehash, check-and-retry) rather than pretending it can't happen.
  • Counter-based: maintain a globally unique, monotonically increasing counter (a dedicated ID-generation service, or a database sequence) and base62-encode that counter's value into the short code. This guarantees uniqueness by construction — no collision handling needed at all — at the cost of needing that counter itself to be a reliable, available service; if it's a single point of failure, the whole write path depends on it.

A counter-based scheme is generally the cleaner answer specifically because it sidesteps collision handling entirely, provided the counter service itself is designed to not become a new single point of failure — a common real answer is to hand out ranges of the counter to each app server ahead of time (server A gets IDs 1–1000, server B gets 1001–2000, and so on), so no single request has to make a synchronous call to a shared counter on the hot write path at all.

Step 5: Trade-offs and Bottlenecks

The cache is doing most of the real work here, which means a cold cache (right after a deploy or a cache-node failure) briefly exposes the database to full read traffic — worth naming explicitly, along with a mitigation like a modest connection-pool cushion on the database sized for exactly that scenario. Custom aliases reopen the collision question the counter-based scheme was designed to avoid, since a user-chosen alias isn't drawn from the counter at all and needs its own uniqueness check against the same table. And a single-region database is a real availability risk worth naming out loud, even if a full multi-region replication design is out of scope for the time available in the interview.