A System Design in Forty Minutes
What a One Line Design Request Actually Asks For
A one line request becomes designable once you have written the one job, the two or three operations with their input and output shapes, and the things you are leaving out.
The request arrives as one line. Build a service that turns long web addresses
into short ones, and tell us how often each short one is clicked. That is a
product description, and nobody can design from it, because it says nothing
about how many, how large, how fast, or what happens when the same thing is
asked twice. The first five minutes turn it into something a drawing can be
checked against.
The one job
A link shortener looks like two products and is one. Ask what has to keep
working for any of it to be worth paying for, and the answer is that a code
handed to the service comes back as the address it stands for. If that stops,
every link ever handed out is dead and the analytics are a record of a service
nobody can use. If the click counting stops, the links still work and somebody
files a ticket.
So the one job is to return the address a code stands for, correctly, for as
long as the code exists. Creating codes serves that. Counting clicks is a
second, smaller product sharing a key with the first, and ranking it second
rather than equal is what later allows a weaker promise about it without that
feeling like a concession.
The operations, written as shapes
Two or three operations, each with what goes in and what comes out. Writing them
as signatures rather than as verbs is the whole trick, because a verb cannot be
multiplied by anything.
def create(target: str, owner_id: bytes) -> str:
"""target is a URL, and RFC 3986 bounds its length nowhere, so the mean
length is an estimate rather than something to look up. owner_id is 16
bytes of fixed width. The return is 7 characters out of a 62 character
alphabet, and that 7 is derived below rather than remembered."""
...
def resolve(code: str) -> tuple[str, int]:
"""code is 7 bytes. The return is the target and the HTTP status to
redirect with. A 302 keeps the client coming back, so every click is
counted; a 301 lets the client remember, so later clicks never arrive."""
...
def clicks(code: str, window: tuple[int, int]) -> list[tuple[int, int]]:
"""window is two timestamps, 8 bytes each. The return is one count per
time bucket, so the answer grows with the width of the window and not
with how popular the link is."""
...The third signature deserves a second look. It returns one count per bucket
rather than one row per click, so its answer is bounded by the window the caller
asked for. An operation whose output grew with the popularity of the link would
have no bound at all, and that difference stays invisible until somebody writes
the output shape down.
How long the code has to be
Here is the step almost everyone skips. The code length is quoted from memory as
six or seven characters, and it is forced by how many distinct links the service
must name over its whole life. Let be that total, the alphabet size and
the code length. The rows below use the hundred million links a day that the
next stage derives, which is the one figure here borrowed rather than worked
out.
- how many distinct links the service must name over its whole life
- how many characters the alphabet has, 62 for digits with upper and lower case letters
- the code length in characters, which must be a whole number, so round up
- how many distinct codes of that length exist, the quantity that has to cover L
| step | code length | distinct codes available | multiple of the row above | what happened |
|---|---|---|---|---|
| 1 | 5 | 916132832 | 1 | Nine hundred and sixteen million. Nine days at a hundred million links a day, then the service is out of names. |
| 2 | 6 | 56800235584 | 62 | The row above times 62. Fifty-six point eight billion, which is a year and a half, not a service life. |
| 3 | 7 | 3521614606208 | 62 | Three and a half trillion, about ninety-six years at that rate. This is the answer. |
| 4 | 8 | 218340105584896 | 62 | The eighth character costs every user a keystroke forever and buys sixty-two times more room than can ever be used. |
Seven, then, with the reason on the page rather than in anybody's memory. The
same arithmetic handles a shrinking alphabet, say because codes must survive
being read aloud and the letters resembling digits have to go. With 32
characters and at , the ratio is 7.5, so
eight characters. One keystroke is the price of being readable.
Who is asking, and how many of them
Three populations, not one. Those that create links are a small set, because
creating one is deliberate work done by a publisher or a marketing tool. Those
that follow links are everybody who sees one, which is the publisher's audience
rather than the publisher. Those that read click reports are a subset of the
first, asking rarely and asking for far more work per question.
| the one job fails without it | it changes an estimate | it adds a box | the request asked for it | |
|---|---|---|---|---|
| resolve a code to an address | 1 | 1 | 1 | 1 |
| create a code for an address | 1 | 1 | 1 | 1 |
| clicks per code over time | 0 | 1 | 1 | 1 |
| custom vanity aliases | 0 | 0 | 1 | 0 |
| expiry dates on links | 0 | 0 | 1 | 0 |
| accounts, teams and sharing | 0 | 0 | 1 | 0 |
| malware and phishing checks | 0 | 1 | 1 | 0 |
The gap between the second population and the first is the most useful number in
the whole scope, and it is a ratio rather than a total. A service whose reads
outnumber its writes by a hundred to one is a different object from one where
they are equal. Write down that the counts are separate and that the second is
expected to be far larger, and the next stage has something to estimate.
The two ways this goes wrong
The first is designing for a job nobody asked for. Nothing feels more like
competence than volunteering expiry policies, vanity aliases and team accounts,
and each is a box that now needs justifying by a number nobody estimated. The
grid above prices it.
The second is quieter. It is drawing before the operations are written down,
which happens because the components are the memorised part. Such a drawing can
be perfectly reasonable and still indefensible, because the only honest answer
to why a box is there is that systems like this usually have one. A shape can be
multiplied. A box cannot.
The one sentence
Fold all of it into one sentence a drawing can be measured against. It needs a
verb, a volume, a bound and an exclusion.
Store an address under a seven character code and return that address on demand,
for a readership expected to be around a hundred times the set of people who
create codes, fast enough that a reader does not notice the hop, while counting
clicks per code accurately to within a few; aliases, expiry, accounts and
content checking are out.
Every number in that sentence is an estimate rather than a measurement, and the
next stage is where they come from. What the sentence does is make those
estimates necessary, which is the only thing that reliably stops a design
becoming a list of components.
What to hold on to
Name one job and rank the others below it, because that ranking is what later
lets a weaker promise be made about the lesser one. Write every operation as an
input shape and an output shape, because the arithmetic that justifies every box
multiplies shapes and cannot be done on verbs. And say out loud what you are not
building, in the second minute, while it is still free.
Recap
- A one line product request becomes a design problem only once the one job, the two or three operations with their input and output shapes, and the things being left out have all been written down.
- The length of an identifier is forced by the logarithm of how many distinct things it must name, taken in the base of its alphabet, so seven characters out of sixty-two name three and a half trillion links while six name only fifty-seven billion.
- The two populations behind a service can differ by orders of magnitude, and that gap rather than the total traffic is what most of the later design turns out to be answering.
This is the reading half
Starting the course gives you your own copy of it. Every idea on every page has problems standing under it, marked with a reason rather than a tick, and any sentence you do not believe can be opened and argued with. None of that can happen on a page nobody owns.
The contents