The problem
A URL shortener turns a long web address into a short one, like sho.rt/zn9edcu. Opening the short link sends you on to the long one. So the service has two jobs: make a short link, and follow one.
The API is the list of requests clients may send. POST /urls with a long URL creates a link and returns its code. GET /zn9edcu answers 301 (a status code meaning moved) with a Location header naming the long URL. The browser goes there.
Rough numbers steer the design. Assume 100 million new links a day and 10 clicks per link. That is about 1,160 writes and 11,600 reads per second, and 3.65 TB a year at 100 bytes per link.
Writing a link
Each link is one row: the short code (the primary key, unique per row), the long URL and when it was made. Every read is a lookup by key with no joins, so a key-value store or one SQL table both work.
To make a code, the app asks the ID generator for the next unique number, 2009215674938, and writes it in base 62 (0-9, a-z, A-Z): zn9edcu. Seven characters give about 3.5 trillion codes, and no two clash.
POST /urls goes to the load balancer, which spreads requests over identical app servers. One app server saves the row (zn9edcu, long URL) in the primary database and replies with sho.rt/zn9edcu.
Reading at scale
A click on a popular link sends GET /qpjF8n6. The app first asks the cache, a store kept in memory (RAM), which answers far faster than a database on disk. It is a hit: the long URL comes back and the app replies 301.
Our new code zn9edcu is not cached yet: a miss. The app reads its row from a replica (a read-only database copy), then puts it in the cache so the next click is a hit. A small cache of popular links serves most reads.
Reads outnumber writes ten to one, so the primary takes every write and streams copies to read replicas. More replicas, more reads per second. The trade-off: a replica can lag a moment behind, so a brand-new link may briefly not be found on a replica.
A 301 may be cached, so a CDN edge server near the user, or the browser itself, can answer repeat clicks without reaching our servers. Use 302 instead if every click must be counted: it is not cached by default.
Trade-offs
Every write still lands on one primary, and one ID generator could fail. Fixes: split the table across several databases by code (sharding), and give each generator its own block of numbers. Each fix adds parts to run and watch.
What happens
;; step 1/11 · goal ;; at: user long example.com/guides/brewing-coffee short sho.rt/zn9edcu job 1 create: long URL → short code (rare) job 2 follow: short code → redirect (very common) design = choosing the parts, and how requests flow through them
The traffic figures are classroom assumptions (100 million new links a day, 10 reads per write, 100 bytes a link), not measurements of any real service. Their job is to show the scale: thousands of reads a second and terabytes a year. 301 is cacheable unless headers say otherwise; 302 is cached only when headers allow it (RFC 9110 and RFC 9111).