CHRISTOPHER YU
BOOK A CALLEMAIL ME
HOW I THINK ABOUT ARCHITECTURE

Boring architecture,chosen on purpose.

Most products do not fail because the framework was wrong. They fail because state ended up in three places, because nobody decided what happens when the payment provider is down, or because someone added a queue in month two and a cache in month three and now nothing is debuggable.

SCOPE
3 products
SHAPE
Same spine
STORE
Postgres
RULE
Add nothing early
01  THE SPINE

Every product I build has the same six parts.

Not because I am incurious, but because the shape is not where the value is. A typed client, one API surface, a relational database holding the truth, a cache only where reads got expensive, workers for anything slow, and third-party services kept at the edges. What changes between products is the domain model, and that is where the thinking should go.

FIG. 01 / THE SPINE
01 CLIENTtyped end to end
02 APIone surface, validated
03 POSTGRESthe only truth
04 CACHElater
05 WORKERS
06 EDGESwrapped, fallible
DASHED = ADDED ONLY WHEN MEASURED, NOT WHEN IMAGINED
02  WHERE STATE LIVES

One place, and everything else derives from it.

The first question on any project is which facts are true and where they are written down. Everything after that is derivation. If a number can be computed from the facts, it gets computed, or it gets stored in something explicitly labelled as derived and rebuildable with one command.

FACTS
Rows a human created or a payment provider confirmed. Written once, corrected by a new row rather than an overwrite where it matters.
DERIVED
Totals, pacing, availability, progress. Recomputed on write where reads are frequent, always rebuildable from facts alone.
EPHEMERAL
Filters, open panels, form drafts. Lives in the client and is allowed to be lost.
NOWHERE ELSE
No business rule in a component. No total that only exists in a chart. No truth in a cache.
03  WHEN TO ADD

Complexity has to be earned by a measurement.

Every extra component is a thing that can fail at 2am, and a thing the next engineer has to understand. So each one needs a trigger, agreed in advance, rather than a hunch.

A CACHE
When a query is slow, frequent, and provably the bottleneck. It must be invalidatable by hand, and the app must be correct without it.
A QUEUE
When work is slow enough to time out a request, or must survive a restart. PDFs, email, imports. Never for speed alone.
A SECOND SERVICE
When two parts of the product genuinely scale or deploy differently. Almost never true in year one.
AN ABSTRACTION
On the third occurrence, not the second. Two similar things are a coincidence.
04  FAILURE CASES

Decided before launch, not during the incident.

Every third party in the system gets the same three questions written down: what does the user see when it is down, what happens to the work that was in flight, and how do I know it happened without a customer telling me.

WHEN THIS IS DOWNTHE USER SEESTHE WORK
Email providerNothing, it is not in their pathQueued, retried, alerted after N failures
Payment providerA plain message, not a stack traceNothing marked paid without a confirmed webhook
File storageUpload disabled, rest of app worksNo half-written records pointing at nothing
The databaseAn honest status pagePoint-in-time backups, restore actually tested
05  THREE PRODUCTS, ONE SPINE

Same shape, different hard parts.

The architecture barely changes between my three products. The domain problem changes completely, and that is where the weeks go.

Monark
FINANCE
The hard part was arithmetic you cannot get slightly wrong. Money as integers in minor units, pacing recomputed on write so the dashboard is one indexed read, and two currencies reading from the same column.
DERIVED ROLLUPSINTEGER MONEYMULTI-CURRENCY
READ THE CASE STUDY →
ClassPortal
TENANCY
The hard part was tenancy, not scheduling. The same records have to serve a tutor working alone and a tutor inside an agency, so every resolver answers the organisation question before it answers anything else, and reassigning a student between tutors has to carry their sessions, assignments and invoices with them.
THREE-ROLE TENANCYMATERIALISED RECURRENCEONE GRAPHQL SURFACEEN / ID
READ THE CASE STUDY →
Stencil
BOOKING
The hard part was that an enquiry and a booking are not the same object. An enquiry is quoted, then accepted or declined. A booking then has a life of its own through deposit, confirmation and completion, and can still be cancelled or no-showed. Collapsing both into one status column would have made every question about money ambiguous.
TWO STATE MACHINES35 RLS POLICIESOWNER / ARTIST ROLESEN / ID
READ THE CASE STUDY →
06  WHAT I REFUSE TO DO

Four things, regardless of who asks.

01
Ship without backups I have restored from. An untested backup is a belief, not a backup.
02
Mark money as received without a confirmed webhook. Optimistic payment state is how you end up refunding strangers.
03
Put a business rule only in the interface. If it matters, the database enforces it too.
04
Hand over a system nobody but me can run. If the docs do not let your next engineer deploy it, the job is not finished.
Want this decided properly on your product?