Dangerous Assumptions
Some assumptions are silently load-bearing: one currency, one timezone, one warehouse, users have accounts, the provider is up, a request arrives once. Each is reasonable for V1, each is invisible in the code, and each turns a requirement change into a redesign. Knowing the list is how you notice yours.
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.
Which assumptions are the ones that hurt — the ones that every V1 makes, that no code names, and that a single ordinary requirement turns into a redesign — and how do you find out which of them your system is standing on?
I have a register, I have been diligent, and I still got blindsided: the payment provider was down for an hour and the store queued nothing, retried nothing and told customers nothing. It never occurred to me to write "the provider is up" as an assumption. What else have I not thought to write down?
Wait for the next one. Assumptions are discovered when they break, the register records them afterwards, and over time the list becomes complete. It is an honest position — you cannot list what you have not noticed — and it feels like the register is working.
The assumptions that hurt most are the ones the register discovers last, because they are the ones that look like facts rather than choices. "Requests arrive once" is not something anyone decides; it is how the code reads until a retry makes it false. Waiting to notice means noticing in production.
- The assumptions that hurt most are the ones the register discovers last, because they are the ones that look like facts rather than choices. "Requests arrive once" is not something anyone decides; it is how the code reads until a retry makes it false. Waiting to notice means noticing in production.
- Each of these is load-bearing in a specific, repeatable way, and the industry has broken every one of them enough times that the pattern is known. Rediscovering them one outage at a time is paying full price for information that is available for free.
- The register fills with the assumptions you were able to see and misses the class of assumption that is invisible by construction — the ones that shaped what is *absent* from the code: no currency field, no timezone, no retry, no idempotency key, no warehouse entity.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Use the known list as a checklist against your own design, not as a lesson about other people's. There is a short set of assumptions that almost every first version makes, that no code names because each one makes something unnecessary, and that a single ordinary requirement contradicts: one currency, one timezone, one warehouse or region, users have accounts, the external provider is up, a request arrives exactly once, one language, prices do not change during a workflow, the clock is right, one tenant. For each, ask: does my system stand on this, and where?
- Recognise the signature. A dangerous assumption shows as an absence — a bare number where a money type would be, a timestamp with no zone, a call to a provider with no timeout and no retry, a handler with no idempotency key, a product with no location. The absence is the assumption's footprint, and the checklist tells you which absences to look for (States That Must Be Unrepresentable).
- Sort by what the contradiction costs. Some flip cheaply if isolated now — a money type at one chokepoint costs an hour and turns "second currency" from a redesign into a local change. Some cannot be isolated and must be designed for or explicitly deferred with a known consequence — "the provider is up" is either handled (External Systems Fail) or accepted as "the store is down when they are". The decision is which of these to isolate now, and it is not "all of them".
- Record the ones you are standing on in the register with their absences as dependents (The Assumption Register), so that "sell in a neighbouring country" reads against "one currency, one timezone, one language" in a minute instead of being discovered field by field.
The checklist, as what each one breaks
Each row is one of the load-bearing assumptions, with the requirement that contradicts it, the footprint it leaves in the code, and the cheapest honest response. The table is a starting checklist, not a complete one; its purpose is to be walked against your own design, one row at a time, asking "am I standing on this, and where?".
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| One currency → a second market, or a customer paying in another currency | Totals add unlike units; the provider is charged in the wrong currency | Price is a bare number; nothing carries a currency | Money as (amount, currency) at one chokepoint; refuse mixed sums; per-currency totals |
| One timezone → customers or staff in a second zone | Sales start at the wrong hour; daily reports split days | Timestamps stored or compared without a zone; the server's day is "today" | Store instants; decide the display and reporting zone explicitly (Time as a Dependency) |
| One warehouse or region → a second location | Stock right in total, wrong per place; shipping from the wrong origin | No location entity; stock as a column | Inventory as (product, location); fulfilment rule as an explicit decision |
| Users have accounts → guest checkout, or an API client | Carts and confirmations have no home | Everything keyed by user id; notification path assumes an account email | Cart keyed by token; contact captured at checkout — or a requirement, if identity is owed |
| The provider is up → an outage, a slow hour, a rate limit | Checkout hangs or fails; nothing queued, nothing told | No timeout, no pending state, no retry, no message | Timeout, pending payment state, retry with idempotency, customer-visible outcome (External Systems Fail) |
| Requests arrive once → a retry, a double click, a redelivered webhook | Double charge, double order, double email | Handlers with no idempotency key; side effects not keyed | Idempotency key per request; unique constraint as backstop (Idempotency by Design) |
| Prices do not change during a workflow → an admin edit mid-checkout | Customer pays a total that does not match what they saw | Order items reference the product's current price | Snapshot prices onto the order at checkout (Snapshots vs References) |
| One tenant → a second business on the same deployment | Data leaks between tenants; reports mix them | No tenant key on any row; queries unscoped | Tenant on every row and every query, or a separate deployment (Multi-Tenancy) |
Where the store stands, and what to do about each
The matrix is the store walked against the checklist. The footprint column is the specific absence; the last column is the sort — isolate now, design for, defer with consequence — and the deferred rows carry the consequence in words, because a deferral without one is a silent assumption reinstated.
| Assumption | Standing on it? | Footprint (the absence) | Contradicting requirement | Sort |
|---|---|---|---|---|
| One currency | yes | bare-number price; hard-coded currency in the provider call | neighbouring country (already proposed) | isolate now: money type at the pricing chokepoint |
| One timezone | yes | raw timestamps; server day in reports | same | isolate now: instants stored, zone decided per customer |
| One warehouse | no longer | — | second location (done) | retired; replacements registered |
| Users have accounts | yes | cart keyed by user id; account email for confirmations | guest checkout | requirement — legal identity; negotiate, do not drop |
| Provider is up | yes | no timeout, no pending state, no retry, no message | any outage (already happened) | design for now |
| Requests arrive once | partly | no idempotency key on confirmation; unique constraint only | any retry (already happens) | design for now: key first, constraint as backstop |
| One language | yes | hard-coded text in templates | second market | defer; consequence: a pass over every template |
| Clock is right | yes | sale start compares server time | a drifted clock | defer; consequence: a sale may start seconds off |
"We need multi-currency support"
The stakeholder's sentence arrives as a feature and hides a much smaller need. The ladder walks it down to what a second currency actually requires of the store today, names the cheap thing that meets it, and — because the device is not "big features are always wrong" — says when the full feature was the right call.
“We need multi-currency support before we sell in the neighbouring country.”
- ↓Why multi-currency? Customers there pay in their own currency and the store only knows one.
- ↓Why does the store only know one? Price is a bare number and the provider call hard-codes the currency — the "one currency" assumption's footprint.
- ↓Why is that a problem for one more country? Because a second currency in the same tables would corrupt totals and reports, and the provider would be charged in the wrong unit.
- ↓Why does that require conversion, per-market pricing and multi-currency settlement? It does not. It requires that a second currency cannot be mixed with the first — which is a money type, per-currency totals and the currency passed to the provider.
the claim was right when The business will price per market from a single base price, display converted prices, or settle in several currencies with finance reconciling exchange differences — then conversion rates, their staleness and their reconciliation are real requirements with an owner, and the full feature is the honest answer.
How to do it
Most important first.
- Walk the checklist against your design and write, for each entry, where the system stands on it — the specific absence: which field, which call, which handler.
- For each you stand on, name the ordinary requirement that would contradict it. If you cannot imagine one within the life of the system, record it and move on.
- Sort into three: isolate now (cheap to fence, likely to flip), design for now (cannot be fenced; money or availability), defer with a stated consequence (unlikely or affordable).
- For "requests arrive once" and "the provider is up", do not defer without saying what the customer sees when they are false; those two are the ones that cost money (Duplicate Requests).
- Add your own entries to the checklist the first time one of yours breaks; the industry list is a start, not a ceiling.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The store against the checklist. One currency: standing on it — price is a bare number, totals add, the provider call hard-codes a code. One timezone: standing on it — raw timestamps, the daily report. One warehouse: was standing on it, retired. Users have accounts: standing on it, but promoted to a requirement with a legal reason. Provider is up: standing on it — the charge call has no timeout, no retry, no "pending" state, and the outage proved it. Requests arrive once: standing on it — the confirmation handler has no idempotency key, and only the unique constraint from Invariants in an Online Store saved it. Clock is right: standing on it — sale start compares server time to an entered time.
- Sorted. Isolate now: currency (a money type at the pricing chokepoint; an hour) and timezone (store instants, display per customer; an afternoon). Design for now: provider up (timeout, a pending payment state, a retry with idempotency, and a customer message — the outage already paid for this lesson) and requests arrive once (idempotency key on the confirmation handler; the constraint was the last line, not the first). Defer with consequence: one language (the store is in one market; consequence written: "all text is hard-coded; a second language is a pass over every template") and clock (the server syncs time; consequence: a sale may start seconds off).
- "We need multi-currency support" from a stakeholder, walked down the why ladder in the section below: what was needed was that a second currency does not corrupt totals — which the isolation step delivers — not that the store converts, prices per market or settles in several currencies. The ladder found the cheap version of the requirement, and the case where the full version is right.
How you know it worked
What now exists that did not before, and what question you can now ask.
- For every entry on the checklist you can say where your system stands on it, as a specific absence — or that it does not.
- Each one you stand on is sorted: isolated, designed for, or deferred with a written consequence.
- The two that cost money — provider is up, requests arrive once — are not in the deferred pile without a stated customer-visible consequence.
- The register gained entries you had not noticed, and their dependents are absences rather than code.
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.
- ?For each of one currency, one timezone, one region, accounts required, provider up, requests arrive once, one language, one tenant — does my system stand on it, and which absence is the footprint?
- ?What ordinary requirement would contradict each one within the life of this system?
- ?Which are cheap to isolate now, which must be designed for, and which can be deferred with a consequence I have written down?
- ?When the provider is down or a request arrives twice, what does the customer see — and have I decided that or defaulted it?
- ?Which assumption is my system standing on that is not on anyone's list yet?
What can go wrong
- Every checklist entry is isolated pre-emptively: a money type, a locale type, a tenant type, a region type, each with one implementation, in a store with one market. The checklist is for noticing; the sort is for deciding, and "defer with consequence" is a legitimate outcome.
- The checklist replaces thinking: the ten entries are checked and the store's own dangerous assumption — "a product has one price" for a B2B tier that is already being discussed — is missed because it was not on the list.
- Assumptions are isolated with abstractions that do not actually fence them. A
Moneytype whose currency is always the same constant has not isolated anything; the test is whether a second value could flow through without touching every consumer.
- Isolating an assumption that never flips is an abstraction paid for nothing; the checklist raises the temptation to fence everything.
- Designing for "the provider is down" adds a pending state, a retry and a customer message to a checkout that was three steps — real complexity, justified by an outage that has already happened or by money at stake.
- The checklist is industry-general and a domain has its own; using only the general list produces confidence that is slightly misplaced.
- "Avoid these assumptions." Make them — they are why V1 ships. The lesson is to know which ones the system stands on, what the footprint is, and which to fence. A V1 store with a money type and one currency is fine; a V1 store that does not know it assumed one is the problem.
- "The provider being up is a reliability concern, not an assumption." It is both. As an assumption it has dependents — no timeout, no pending state, no message — and a false-when; naming it as one is what puts it in front of the design instead of in front of the on-call engineer (When the Provider Fails).
- "If the register is complete, the checklist is redundant." The register holds what you noticed; the checklist is a list of what people reliably fail to notice. They are complementary, and the checklist is how the register gets the entries that would otherwise arrive as outages.
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 checklist entries recur across almost every product-shaped system because they are all consequences of building for one market, one deployment and one happy path first; a pipeline or a compiler has its own analogues — one encoding, one input file, one machine — and the same move applies.
- CONTESTEDA strong school holds that fencing assumptions before they flip is the definition of over-engineering: a money type in a one-currency store is speculative generality, YAGNI applies, and the honest estimate for adding a currency later is usually smaller than the accumulated cost of abstractions that were never used. The strongest version of that view is that most of these assumptions never flip in most systems, and the ones that do are cheaper to handle when the requirement is concrete than to anticipate in the abstract. The position here is narrower than "fence them all": isolate the cheap and likely ones, design for the two that cost money, and defer the rest with a written consequence — which is a judgment made per system, and the other school's point is why the deferred pile exists.
- ILLUSTRATIVEThe store's outage, the hour for a money type and the afternoon for timezones are invented to show the sort; real costs depend on how many consumers the absence has, and the checklist itself is a starting set rather than a canonical ten.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — The technology decision tool at /thinking/decide asks "which problem are you solving?" of every component; the same question, asked of every assumption on this list, is the sort in the matrix above.