This walks the full five-step framework from chapter 1 against a single concrete problem, start to finish.
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.
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.
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.
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:
[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.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.
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.