Case: A URL Shortener
Long URL → Short Code → Redirect. The first version is one table and two endpoints; the interesting engineering arrives only when the requirements do — uniqueness under concurrency, read-heavy traffic, click logging — and the ladder for "we need a distributed ID generator" ends somewhere much smaller.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
The URL shortener is the system-design interview's favourite, so you already know the "final" architecture. How do you build the first version without it, and what would have to be measured before any of it is justified?
Marketing wants short links for campaigns. You have seen the diagram — hash function, key-generation service, distributed cache, replicated store, analytics pipeline — and it is hard to un-see. What you have not seen is a requirement that any of it exists for, and you suspect the first version is smaller than the diagram by an order of magnitude.
Design the "final" version because you know it. Pick a base-62 encoding, decide how many characters of code you need for billions of links, sketch the key-generation service so that codes can be minted without a database round-trip, put a cache in front of the redirect. It is a well-trodden design and it feels responsible to do it properly.
The design answers a scale question nobody has asked. Marketing sends a few campaigns a month; the diagram is for a public shortener with a global audience, and every component in it is an operational bill for a load that may never arrive.
- The design answers a scale question nobody has asked. Marketing sends a few campaigns a month; the diagram is for a public shortener with a global audience, and every component in it is an operational bill for a load that may never arrive.
- The key-generation service solves "the database is the bottleneck for minting codes" — a problem that does not exist while one table can mint codes with a unique constraint. Its own failure modes (what if the service is down? what if two instances hand out the same range?) are now yours.
- The actual requirement — "marketing can tell which campaign a click came from" — is a click log with a campaign tag, and it is not on the diagram at all. The interview design optimises the redirect and forgets why marketing wanted the link.
- The first version takes weeks because it is the final version. The one table and two endpoints that would have answered every question marketing has would have taken an afternoon.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Reduce the sentence to its three nouns and one verb: a Long URL, a Short Code that stands for it, and a Redirect that takes a code to its URL. That is the whole system, and the first version is exactly one table and two endpoints (The Simplest Thing That Could Work).
- Find the requirement behind the request. Marketing did not ask for shortness; they asked to know which campaign a click came from. The click log is core; the code is the handle for it (What Am I Actually Trying to Achieve?).
- List the ways the one-table version can be wrong, and attach the evidence that would show it: codes collide under concurrent creation; the redirect gets slow under read load; the table grows. Each is a real failure with a real fix, and each fix waits for its evidence (Scale Thought Experiments).
- Run the why ladder on every component from the interview diagram before adding it. Most rungs end at a unique constraint, an index, or an in-process cache; the distributed ID generator has a real justification, and it is not this one.
Three nouns, one verb, one table
The first version in full. It is short enough to read in one screen and it meets marketing's requirement, which the interview diagram never mentioned. The unique constraint is the only line that does any engineering, and it is the line the test targets.
1CREATE TABLE link (2 code TEXT PRIMARY KEY, -- random, short; the constraint is the uniqueness guarantee3 long_url TEXT NOT NULL,4 campaign TEXT, -- the requirement marketing actually had5 created_at TIMESTAMPTZ NOT NULL DEFAULT now()6);7 8CREATE TABLE click (9 code TEXT NOT NULL REFERENCES link(code),10 clicked_at TIMESTAMPTZ NOT NULL DEFAULT now(),11 referrer TEXT12);13 14-- create: INSERT a random code; on unique violation, generate another and retry15-- redirect: SELECT long_url FROM link WHERE code = $1; INSERT INTO click ...; 30216-- marketing's question:17SELECT campaign, date_trunc('day', clicked_at) AS day, count(*)18FROM click JOIN link USING (code)19GROUP BY campaign, day;The primary key is the invariant. The retry is the only concurrency code. Everything in the famous diagram is absent because nothing here has asked for it yet.
"We need a distributed ID generator"
The ladder was run when the generator was proposed. It is the case's central device because the generator is the component people most confidently add to a shortener and least often need. The ladder ends with the case where it was right, because it sometimes is (Distributed Uniqueness: One Name, Many Shards in Distributed covers that case).
“We need a distributed ID generator for short codes.”
- ↓Why a generator? So we can mint codes without a database round-trip, and so they never collide.
- ↓Why avoid the round-trip? Because the database would become the bottleneck for creating links.
- ↓Is it the bottleneck? Creates are a small fraction of requests; redirects dominate; the write path is idle by comparison.
- ↓Why never collide? Because a collision would send someone to the wrong URL — which the unique constraint already prevents, by rejecting the second insert.
the claim was right when Link creation itself is the dominant load, or codes must be minted in several regions or offline without contacting a shared database. Then a coordination-free scheme — pre-allocated ranges, or a structured id with a node component — is the simpler thing, and the constraint becomes a backstop.
Scale, progressively
The famous diagram is a ledger, not a plan. Each row below is one of its components with the reading that would put it in the code. The redirect is read-heavy and the click log is write-heavy, and they scale differently — which is why the first real change was to the write on the redirect path, not to the read.
| Component from the diagram | Problem it solves | Reading that would justify it here | Simpler thing tried first |
|---|---|---|---|
| Cache in front of the redirect | Hot codes read far more than the database can serve | Redirect latency dominated by the lookup at a volume the database cannot serve, with a small set of codes taking most reads | Index on code (already the primary key); measure the plan |
| Async click logging | The click write slows the redirect | Redirect time dominated by the click insert | Insert into a jobs table and return; a worker batches clicks (Background Jobs and Workers) |
| Read replica | Redirect reads saturate the primary | Primary CPU dominated by redirect reads after the index and cache | Cache first; a replica adds replication lag to a path that has none |
| ID generation service | Creates saturate the write path or must be coordination-free | Create latency or contention on the unique constraint under measured load | Random code with constraint and retry |
| Analytics pipeline | Marketing queries slow the production database | The grouped query measured as a load problem, or a freshness requirement it cannot meet | The query as written; a nightly summary table (Case: An Analytics Dashboard) |
How to do it
Most important first.
- Table: code (unique), long_url, campaign, created_at. Endpoint one: create a link, returning the code. Endpoint two: GET /{code} redirects and appends a click row. That is V1 (The Smallest Executable Thing).
- Generate the code from a random string and rely on the unique constraint to reject a collision; retry on conflict. Test the collision path by forcing a duplicate (Invariants as Tests).
- Write the click log first-class: code, time, referrer, whatever marketing needs to answer their question. Then write the query that answers it, because that is what the system exists for.
- Ship, then measure the redirect path under real traffic. Read the plan for the lookup, check the index is used, and only then decide whether a cache is warranted (Should I Add an Index? in Database is the right lesson).
- Keep the interview diagram as the ledger of what might come next — each component with the reading that would justify it (The Complexity Ledger).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- V1 in an afternoon. One table, two endpoints, a random six-character code, a unique constraint and a retry. The campaign tag is a column. Marketing's question — clicks per campaign per day — is one grouped query over the click table. Nothing in the interview diagram was needed, and the goal was met.
- The first real failure. Two campaign links created at the same instant received the same random code once in a test run with the constraint disabled; with it enabled, one insert failed and the retry produced a fresh code. The invariant — codes are unique — lives in the database, not in the generator, and the test that forces a duplicate is kept (Invariants Under Concurrency).
- The why ladder for the ID generator, run when someone proposed it. "We need a distributed ID generator" → why? → so codes can be minted without hitting the database → why is that needed? → because the database would be the bottleneck for creates → what did the measurement say? → creates are a tiny fraction of traffic; redirects dominate; the database is nowhere near its limit. Real requirement: unique codes. Simpler: the constraint already there. Justified when: creation volume genuinely saturates the write path, or codes must be minted offline or across regions without coordination.
- Scale, progressively and on readings. The first bottleneck under real traffic was the redirect query, and the first reading showed the index was used and the query was fast — the slowness was in the click insert on the same request. The response was to write the click asynchronously to a jobs table, not to add a cache to the redirect, because that is what the evidence named. A cache for hot codes went on the ledger with its trigger: redirect latency dominated by the lookup, at a read volume the single database cannot serve.
How you know it worked
What now exists that did not before, and what question you can now ask.
- The first version exists, marketing has used it, and their question is answered by a query you can show them.
- Codes are unique because of a constraint you tested, not because of a generator you trust.
- Every component from the interview diagram is on a ledger with the reading that would justify it, and none of them is in the code.
- The first scaling change was to the part the measurement named, and it was smaller than anyone expected.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?What are the three nouns and one verb, and what is the one-table version of them?
- ?What did the requester actually want to know, and does the smallest version answer it?
- ?Which invariant does the simplest version rely on, and where is it enforced?
- ?For each component in the famous diagram, what reading would justify it here?
What can go wrong
- The one-table version is shipped without the unique constraint because "collisions are astronomically unlikely". Unlikely is not never; the constraint costs nothing and turns a silent overwrite into a retry.
- The click log is added "later" and marketing runs their first campaign with a shortener that cannot answer their only question.
- Scale-later is used as an excuse to skip measurement, and the redirect gets slow with nobody watching. The move requires a reading, and readings require instrumentation from V1.
- The interview diagram is dismissed entirely. It encodes real solutions to real problems at real scale; the case argues for the order and the evidence, not for pretending the problems never arrive.
- Random codes with a retry cost an occasional extra round-trip on collision; sequential codes never collide but leak creation order and count. The case chose random because the leak mattered to marketing and the collision did not.
- A single table that holds both links and clicks is simple and will one day be dominated by clicks; splitting it later is a migration under load. The bet is that the migration is cheaper than designing for it now.
- Deferring the cache until a reading asks for it means the first slow redirect is seen by a user. The alternative — caching from the outset — hides the read pattern you would have needed to size the cache.
- "So the interview design is wrong." It is a correct design for a global public shortener at very high read volume. It is wrong as a first version for a marketing team, and the difference is the requirements, not the design.
- "Random codes are unsafe." They are as safe as their length and the constraint that backs them; the constraint is the safety, and the case tests it.
- "A URL shortener is trivial." The first version is. Uniqueness under concurrency, read-heavy scaling, abuse, expiry and analytics are all real; they arrive as requirements, one at a time, and each is a lesson somewhere else.
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- GENERALThree nouns, one verb, one table, and a ledger for the famous diagram: the approach applies to any system whose reference architecture is better known than its requirements — rate limiters, feed services, notification systems.
- SCALE-SPECIFICFor a public shortener expecting very high read traffic from the launch — a link in a broadcast, say — the redirect cache and a read replica are justified before the first reading, because the reading is predictable. The case is the marketing-team version, where it is not.
- ILLUSTRATIVEThe marketing team, the six-character code, the collision in the test run and the click-insert finding are invented for the shape of the argument; no traffic figures are being reported.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — The architecture-growth lab at /thinking/grow makes the same argument with numbers: raise traffic on the single server and see which component the readings justify, and which they do not.