CasesGENERALSCALE-SPECIFICILLUSTRATIVE

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 moveWorked exampleNext questions

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 question

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?

The situation

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.

The reflex

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.

Why it stalls

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.

What the reflex produces — and fails to produce
  • 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.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

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.

V1: the whole shortener
1CREATE TABLE link (
2 code TEXT PRIMARY KEY, -- random, short; the constraint is the uniqueness guarantee
3 long_url TEXT NOT NULL,
4 campaign TEXT, -- the requirement marketing actually had
5 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 TEXT
12);
13
14-- create: INSERT a random code; on unique violation, generate another and retry
15-- redirect: SELECT long_url FROM link WHERE code = $1; INSERT INTO click ...; 302
16-- 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).

The ID generator, on the ladder

We need a distributed ID generator for short codes.

  1. Why a generator? So we can mint codes without a database round-trip, and so they never collide.
  2. Why avoid the round-trip? Because the database would become the bottleneck for creating links.
  3. Is it the bottleneck? Creates are a small fraction of requests; redirects dominate; the write path is idle by comparison.
  4. Why never collide? Because a collision would send someone to the wrong URL — which the unique constraint already prevents, by rejecting the second insert.
real requirement Codes are unique, and creating a link is fast enough for a person clicking a button.
simpler A random code with a unique constraint and a retry on conflict. The database is the coordinator, and it is already there.

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 diagramProblem it solvesReading that would justify it hereSimpler thing tried first
Cache in front of the redirectHot codes read far more than the database can serveRedirect latency dominated by the lookup at a volume the database cannot serve, with a small set of codes taking most readsIndex on code (already the primary key); measure the plan
Async click loggingThe click write slows the redirectRedirect time dominated by the click insertInsert into a jobs table and return; a worker batches clicks (Background Jobs and Workers)
Read replicaRedirect reads saturate the primaryPrimary CPU dominated by redirect reads after the index and cacheCache first; a replica adds replication lag to a path that has none
ID generation serviceCreates saturate the write path or must be coordination-freeCreate latency or contention on the unique constraint under measured loadRandom code with constraint and retry
Analytics pipelineMarketing queries slow the production databaseThe grouped query measured as a load problem, or a freshness requirement it cannot meetThe 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.

Next questions
  • ?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

How the move itself fails
  • 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.
What the move costs
  • 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.
Misreads
  • "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.

Observability & Performancemeasure-before-optimizingload-testing
Further
  • 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.