The Complexity Ledger
Every new component answers three questions in writing: what problem does it solve, what complexity does it introduce, what happens if it fails. A component that cannot answer all three is not yet justified; one that can is accountable for as long as it lives.
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.
Someone proposes adding a component to the store — a cache, a queue, a search engine, a second service. What must be written down before it goes in, and what does that record let you do later that nothing else does?
A colleague wants to add a search engine for the product catalog. It is a reasonable idea, it is a well-known tool, and you can feel the discussion heading toward "why not?". You cannot articulate an objection, and "it seems like a lot" is not one.
Decide by reputation. The tool is standard, other stores use it, the colleague has used it before. The question becomes whether the tool is good, and it is, so it goes in. Nobody asks what it is for here.
"Is the tool good?" is a question about the tool, and the ledger's questions are about the store. A good tool solving a problem the store does not have is still a component with no reason, and its reputation does not reduce the operational surface it adds.
- "Is the tool good?" is a question about the tool, and the ledger's questions are about the store. A good tool solving a problem the store does not have is still a component with no reason, and its reputation does not reduce the operational surface it adds.
- The failure mode is never discussed because nobody asked. When the search engine is down, does the catalog page fail, fall back to the database, or show nothing? The decision gets made at the first outage, by whoever is on call, in the worst possible conditions.
- The complexity is counted only in setup time. The index that must be kept in sync with the product table, the reindex on every price change, the second data store whose contents can disagree with the first — none of that was in "why not?".
- Six months on, nobody can say whether the component is earning its place, because nothing was written down to compare against. Removing it would require a case; adding it required none.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Before any component enters the architecture, write three answers beside it. What problem does it solve — stated as a symptom observed or a requirement quoted, not as a capability the tool has. What complexity does it introduce — the new data store, the sync, the deployment, the failure modes, the concepts the team must now understand. What happens if it fails — for the user, for the data, and for the team at two in the morning (Add Complexity Only When Required).
- Refuse the component when any answer is missing, and accept it when all three are present and the first outweighs the second and third. The ledger is not a veto; it is the form of the argument. A search engine that solves a measured problem, whose sync complexity is understood and whose failure degrades to a database query, is justified — and the ledger is what says so.
- Keep the ledger for the life of the component, and reread it when the requirements or the load change. The entry that justified a component can stop being true — the symptom disappears, the smaller change becomes available — and the ledger is what makes that visible. A component with a ledger can be removed; one without can only be tolerated.
- Use the ledger to compare alternatives, not only to accept or reject one. The search engine and "a database full-text index" both answer the three questions; laid side by side, the answers decide, and the decision is recorded with both columns (The Trade-Off Matrix, Without Fake Precision).
The ledger, as data
The matrix is four proposals against the three questions. Read the verdict column last; it follows from the others, which is the point. Two are accepted and two declined, and the declined ones carry the condition for their return, so the ledger is also the pending list.
| Component | Problem it solves | Complexity it introduces | If it fails | Verdict |
|---|---|---|---|---|
| Search engine | Substring search on a few thousand names — already served by the database text index | Second store, sync on every product change, reindex job, consistency question, deployment | Catalog page fails or falls back — fallback must be built | Not yet; returns if fuzzy/faceted search becomes a requirement or the index measures too slow |
| Cache for product reads | Measured: burst saturates the database on reads with the query indexed | Cache process, cache-aside path, invalidation on price and stock change, staleness as a concept | Reads fall through to the database; the burst symptom returns; store slows, does not break | Justified, with invalidation designed before shipping |
| Queue + workers for email | Measured: pending-email job lags; wait times exceed tolerance | Broker, worker pool, at-least-once and therefore idempotent sends, dead letters, liveness | Broker down → emails delay, orders unaffected; worker crash → duplicate unless idempotent | Justified; idempotent send is part of the same loop |
| Checkout as a separate service | None observed — "so it can scale independently" | Two deployments, a network hop in the checkout path, distributed failure in the core workflow | Catalog up, checkout down: a store that cannot sell | Not yet; returns on a measured scaling difference or a team boundary |
"Checkout should be its own service" on the ladder
The proposal with no observed problem is the one to climb. The ladder finds the requirement underneath — there is one, and it is not the one stated — and names the case where a separate service would have been the right answer, because sometimes it is.
“Checkout should be its own service so it can scale independently.”
- ↓Why does checkout need to scale independently? Because it is the most important path and must not be slowed by catalog traffic.
- ↓Why would catalog traffic slow checkout? Because they share a server and a database.
- ↓Why is sharing a problem — has checkout been slow under catalog load? Not measured. Under the staging burst, catalog reads were the load and checkout latency did not move once the product query was indexed.
- ↓Why, then, is the proposal on the table? Because a separate service is what checkout looks like in the architectures we have read. The real worry is that a catalog problem could take checkout down.
the claim was right when A measured load shows checkout and catalog need different scaling — for instance catalog reads saturate servers that checkout barely uses — or two teams own them and the deployment cadence of one is blocking the other. Then the boundary is a requirement, and the ledger entry writes down the hop, the distributed failure and the fallback it now owes.
The ledger as a page in the notebook
The entry is short enough to keep and specific enough to be wrong. This is the cache's, on the day it was accepted; the last two lines are what make it a ledger rather than a memo — the condition under which the entry stops being true, and the date it will be reread.
1COMPONENT cache in front of product reads2PROBLEM staging burst (newsletter-sized) saturates DB on reads3 with product lookup already indexed; p95 page time over tolerance4COMPLEXITY cache process; cache-aside read path in backend;5 invalidate on product price/stock change (admin + checkout);6 staleness now a concept the team must hold7IF IT FAILS cache down -> reads fall through to DB -> burst symptom returns;8 store slows, does not break; no data at risk9SMALLER index (done, insufficient at this burst)10VERDICT accepted; invalidation shipped in the same loop11STOPS BEING TRUE if read pattern spreads across many products,12 or if burst size falls below what the DB alone served13REREAD at the next traffic doubling, or when the catalog changes shapeNothing here is about whether caches are good. Every line is about this store, and every line could be shown to be false by a measurement — which is what makes the entry worth keeping.
How to do it
Most important first.
- Write the three questions as headings on a page and fill them in for the proposed component before the meeting, not during it. Missing answers are the agenda.
- For "what problem", demand a symptom or a sentence: a measurement on this system, or a requirement with a source. A capability of the tool is not a problem of the store.
- For "what complexity", list the nouns the team must now know: a new data store, a sync process, a consistency question, a new deployment, a new dashboard. Each noun is a cost with a name.
- For "what if it fails", walk one customer through the system with the component down. Write what they see, what happens to the data, and who is paged (External Systems Fail).
- File the ledger with the architecture and read it when the load doubles or a requirement changes; a component whose entry has gone stale is a candidate for removal (The Decision Journal).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The search engine, on the ledger. Problem: "search products by name" is a requirement; the measured need is a substring match on a few thousand product names, which the database's own text index serves in well under the tolerance. Complexity: a second data store; a sync from the product table on every create, price change and stock change; a reindex job; the question of which store is right when they disagree; a new deployment and dashboard. Failure: with the engine down, the catalog page either fails or falls back — and the fallback has to be built. Verdict: not yet. The database index meets the requirement; the ledger records the condition under which the engine returns — fuzzy or faceted search as a requirement, or the database index measured too slow.
- The cache, on the ledger, after the burst. Problem: a measured burst saturates the database on reads with the product query already indexed. Complexity: a cache process, a cache-aside path in the backend, invalidation on price and stock change, and the concept of staleness the team must now hold. Failure: cache down → every read goes to the database → the burst symptom returns; the store slows, does not break. Verdict: justified, with invalidation written before the cache ships, and the entry kept so that if the read pattern changes the cache can be reconsidered.
- The queue, on the ledger, in month four. Problem: the pending-email job lags behind order volume; wait times measured in the table exceed what customers tolerate. Complexity: a broker, a worker pool, at-least-once delivery and therefore idempotent sends, a dead-letter path, worker liveness monitoring. Failure: broker down → emails delay, orders unaffected because the pending row is written in the checkout transaction; worker crash mid-send → a duplicate email unless the send is idempotent. Verdict: justified; the ledger names the idempotency work as part of the same loop, not a later one.
- A second service, on the ledger. Problem: none observed — the proposal is "checkout should be its own service so it can scale independently". No reading shows checkout limiting anything. Complexity: two deployments, a network hop inside the checkout path, distributed failure in the store's most important workflow. Failure: catalog up and checkout down is a store that shows products nobody can buy. Verdict: not yet; the condition for return is a measured scaling difference between checkout and the rest, or a team boundary.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every component beyond the derived minimum has a written entry with three answers, and the first answer cites a symptom or a requirement rather than a capability.
- Failure behaviour was decided before the component shipped, not at the first outage.
- At least one proposed component was declined on the ledger with the condition for its return written down — and at least one was accepted on it.
- Rereading the ledger after a change in load or requirements is a scheduled act, not an accident.
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 problem does this component solve — as a symptom I have observed or a requirement I can quote?
- ?What will the team now have to understand, run, sync, monitor and explain that it did not before?
- ?If this component is down, what does a customer see, what happens to the data, and who is paged?
- ?Under what change in load or requirements would this entry stop being true, and when will I reread it?
What can go wrong
- The ledger as a form filled to pass review. Three answers are written, the first is "improves performance", the second is "minimal", the third is "graceful degradation", and nothing has been examined. The answers must be specific enough to be wrong.
- The ledger applied to every change. A new column or a new endpoint is not a component; the ledger is for things that add a deployment, a data store, a process or a failure mode. Applied to everything, it becomes a tax nobody pays honestly.
- The ledger written and never reread. It justified the cache at launch; at ten times the load the cache is essential and at a tenth of the load it is a liability, and nobody looked. An unread ledger is a decision journal that stopped being one.
- The ledger used only to refuse. A team that has never accepted a component on the ledger has stopped measuring, or has confused the ledger with a rule that small is right.
- The ledger slows every architectural addition by the time it takes to write three honest answers, and on a component that is obviously right that time is overhead.
- A written failure analysis commits the team to building the fallback it describes, which is work the reflex never budgeted.
- Declining a component on the ledger is a visible disagreement with whoever proposed it, and the ledger has to be the thing that is arguing, not you.
- "The ledger is how you say no." The ledger is how you say why, in both directions. The cache and the queue above were both accepted on it.
- "What happens if it fails is an operations question for later." It is the question that decides whether the component belongs in the checkout path at all, and later is the outage.
- "Complexity means lines of code." The second column counts concepts, processes and failure modes the team must now carry. A component with little code and a sync process is more complex than a large library with none.
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.
- GENERALThe three questions apply to any component in any system, from a cache in a store to a coprocessor on a board; what differs is what counts as a symptom and how the failure column is walked.
- TEAM-SPECIFICOn a solo project the ledger is a note to yourself and can be three lines; on a team it is the argument that survives the meeting, and it has to be specific enough that someone who was not there can disagree with it. Under a deadline the ledger is where "we know this is a shortcut" gets written, so the shortcut can be found later.
- ILLUSTRATIVEThe search-engine proposal, the few thousand product names, the month-four queue and the checkout-service proposal are invented to show the ledger being filled; no real system is described.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — The Engineering Notebook at /thinking/notebook has a decisions section built for exactly these entries: evidence, alternatives, and when to revisit.