SystemsGENERALSCALE-SPECIFICILLUSTRATIVE

Change Propagation

Every component that reads a fact is a component the change must reach. Counting the readers, and the hops between writer and reader, tells you what a change costs and where it will be wrong — before it is made.

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

How far does a change travel through the system, and how do you estimate that before making it?

The situation

The tickets are all small: "show prices with tax included", "rename Cart to Basket", "make order numbers start at a prefix". Each looks like an hour. Each has turned into a week, and the week is always spent finding the fourth and fifth place that also had to change. I want to know in advance which of these is actually small.

The reflex

Estimate from the ticket. The change is described as one thing, so it is one edit; open the file the ticket obviously points to and start. The unexpected places will be found by tests, or by the reviewer, or by the customer.

Why it stalls

The obvious file is changed and the feature works on the page the ticket named. The second page, the export job and the email template still show the old behaviour, and each is discovered by someone else — so "an hour" is reported as done and then reopened three times.

What the reflex produces — and fails to produce
  • The obvious file is changed and the feature works on the page the ticket named. The second page, the export job and the email template still show the old behaviour, and each is discovered by someone else — so "an hour" is reported as done and then reopened three times.
  • The reviewer cannot judge whether the change is complete, because the PR shows what was touched and not what *should* have been. Review becomes "looks fine" and the missing readers survive.
  • Because the cost of a change is never known before it is made, every estimate is the same estimate — small — and the team stops believing its own estimates. Prioritisation then happens by loudness.
  • The fix for the fifth missed reader is a helper that centralises the logic. It is added *after* the week has been lost, and it is added to the one fact that hurt, so the sixth fact starts the cycle again.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Before touching code, classify the change by what it changes: a *value* (a price), a *shape* (a field added or renamed), a *rule* (how a total is computed), or a *boundary* (what an external system sends). Each class propagates differently: values travel along reads, shapes travel along every consumer of the type, rules travel to every place the rule was duplicated, boundaries travel to every adapter.
  • Count the readers and the hops. Readers are the places the changed thing is consumed; hops are the copies between writer and reader — a cache, a snapshot, a message, a report table. Cost scales with readers; risk scales with hops, because every hop is a place the old value can survive.
  • Ask which readers can be *told* and which must be *found*. A typed field renamed is told to every consumer by the compiler. A rule duplicated in three places has to be found by grep and memory. The second kind is the one to estimate generously and to reduce.
  • Write the list of readers into the ticket as the definition of done. The change is complete when every reader on the list has been visited, not when the named page works.

Four kinds of change, four kinds of search

The move starts with classification because the search depends on it. A value change is found by following reads; a shape change by following the type; a rule change by finding the duplicates; a boundary change by finding the adapters. Mistaking one for another is how the export job gets missed — it was searched for as a type consumer when it was a rule duplicate.

The why ladder below is the mechanism proposal that usually follows a week lost to propagation. It is climbed before the proposal is accepted, because the readers were the problem and a mechanism does not reduce them.

Why ladder

We need an event bus so that changes propagate automatically.

  1. Why an event bus? So that when a price or a rule changes, every component that cares is notified instead of us having to find them.
  2. Why do we have to find them? Because the tax rule is duplicated in the catalog, the cart, the email template and the export, and each copy has to be edited.
  3. Why is it duplicated? Because each reader formats money itself; there is no single owner of the rule.
  4. Would an event bus fix that? No — each subscriber would still hold its own copy of the rule and would still have to be edited; the bus only tells them something changed.
real requirement A rule with many readers must have one owner, so that changing it is one edit and the readers cannot drift.
simpler One money formatter that every reader calls. The tax rule becomes a one-place change, with no infrastructure.

the claim was right when The readers are in separate services owned by separate teams that deploy independently, and the thing propagating is a value (a price change) that each service must react to at its own pace — then a published event is the honest mechanism, and the rule-ownership problem is still solved separately.

ChangeTravels alongWho tells youStore exampleWhere it hides
Valueevery read of the factnobody — grep and the propagation mapa price changessnapshots, caches, report tables
Shapeevery consumer of the typethe compiler, inside one codebaseorder number gets a prefixtemplates, other services, stored records at the provider
Ruleevery copy of the rulenobody — the copies do not know about each othertax-inclusive pricesthe email template with its own formatter
Boundaryevery adapter to the external systemthe provider's changelog, if you read itthe webhook payload changesthe parser that assumed the old shape

Estimating the tax ticket

The tradeoff matrix is the honest version of the estimate: not one number, but the shape of the choice between changing nine readers in place and giving the rule an owner first. The numbers are ordinal — they say which option is better on which axis, and no more.

"Show prices with tax included"
OptionSimplicityTimeMaintainabilityReliabilityNote
Change each of the nine readersFast to start, and the tenth reader — found later — is guaranteed to be wrong. Nine chances to format money differently.
One money formatter, then change the rule onceA refactor first, then a one-line rule change; every future money-display rule is a one-place change.
Compute tax-inclusive price in the database viewCentralises the rule but moves display logic into storage; the payment request must still strip it, and the email template still needs to know.

caveat The scores compare these three options for this ticket in a small store. They do not say how long anything takes, and the "refactor first" option loses on time only until the second money-rule ticket arrives, at which point it has already won.

The order-number prefix, followed through the boundary

Boundary changes are the ones that outlast the deploy. The pipeline follows the prefixed order number out to the payment provider and back, and the step that fails is the one the ticket never mentioned: the provider stores our reference and will send the old format in webhooks for orders placed before the change, for as long as those orders can be refunded.

Where an order number goes
  1. 1
    Checkout creates the order

    assigns the new prefixed number

    fails by nothing here — the change is trivial at the writer

  2. 2
    Payment request

    sends the number as the provider reference

    fails by the provider's reference field has a length or character limit the old format never hit

  3. 3
    Provider webhook

    returns the reference to identify the order

    fails by old orders return the old format; a parser that only accepts the prefix drops their refund events

  4. 4
    Shipping label and accounting export

    print and file the number

    fails by a downstream system with its own validation rejects the new shape

  5. 5
    Customer support search

    finds orders by number

    fails by search matches on exact string and customers type the number without the prefix

The estimate for this ticket is the webhook step. Everything else is an hour; that step is "both formats, forever, with a test for each" (Treating External Systems as What They Are).

How to do it

Most important first.

  • Classify the ticket: value, shape, rule or boundary. Say which out loud; it changes the search you are about to do.
  • List readers by two searches — the identifier in code, and the human word in templates, emails, exports, docs and dashboards. Include the readers that are outside the repository.
  • For each reader, mark the hops: does it read the owner, a copy, a cache, a message, a report table? Each hop is a place a stale value lives and needs a plan — invalidate, re-derive, back-fill or accept.
  • Turn "rule" changes into a single-owner refactor first, if the rule is duplicated (Duplicate Knowledge), and only then change the rule. Changing a rule in three places is three changes, not one.
  • Put the reader list in the ticket. The reviewer checks the list, not the diff.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • "Show prices with tax included." Class: rule — the display rule for money. Readers found by searching for the price formatter and for the word "price": catalog, product page, cart, checkout, order history, invoice email, admin product list, CSV export, and the payment provider request (which must *not* include tax twice). Hops: the invoice email renders from a template with its own formatter. Estimate: not an hour, and the first step is one money formatter that all nine readers call — after which the tax rule is a one-place change forever.
  • "Rename Cart to Basket." Class: shape, for the UI copy; the data type keeps its name. Readers: every string a customer sees, plus the URL, plus the analytics event name, plus the support team's macros. The compiler tells us about none of them. Estimate: small in code, and the analytics rename is the hidden cost because the dashboard reads a copied event name that has been stored for a year.
  • "Order numbers get a prefix." Class: shape of an identifier. Readers: order confirmation, admin search, the payment provider's reference field, the shipping provider's label, the accounting export. Hops: the payment provider stores our reference and sends it back in webhooks, so the parser of inbound webhooks must accept both formats forever. That one hop is the whole estimate.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • The ticket has a reader list before it has a branch, and the estimate references it — "nine readers, one of them outside the repo".
  • Review asks "did you visit the export job?" because the list says so, not because someone remembered.
  • Rule changes get cheaper over time, because each one is preceded by giving the rule a single owner.

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
  • ?Is this a value, a shape, a rule or a boundary changing — and therefore what am I searching for?
  • ?How many readers, and how many of them will the compiler tell me about?
  • ?Where between the writer and each reader can the old value survive, and what is the plan for each such place?
  • ?If this rule lives in more than one place, should the first change be giving it one owner?

What can go wrong

How the move itself fails
  • The reader list becomes an excuse. Every ticket is declared large because *something* might read the fact; the classification step exists so that a value change with two readers is called small and shipped.
  • Hops are eliminated on principle. A cache, a snapshot and a report table are hops with a purpose; the move is to know they exist and plan for them, not to remove them so that propagation is simpler.
  • Only code readers are counted. The support macro, the accounting spreadsheet and the customer's bookmark are readers too, and they are the ones that produce the angry email.
What the move costs
  • The reader search takes time proportional to the size of the system, and it is repeated per change; a small codebase with a good compiler gets most of it for free.
  • Reader lists in tickets go stale if a change is delayed; the list is a snapshot of the system on the day it was written.
  • Giving a rule a single owner before changing it is a refactor on the critical path of a feature, and under deadline it is tempting — sometimes correctly — to change the three copies and file the refactor.
Misreads
  • "Fewer readers is always better, so hide everything behind one service." A fact with one reader is a fact nobody uses. The number of readers is a property of the product; the move manages it, it does not minimise it.
  • "This is change amplification and the fix is better architecture." Sometimes. But most propagation cost in a store is facts that legitimately have many readers; the architecture fix is giving each rule one owner, which is a refactor, not a redesign. Change Amplification covers the case where the structure really is the problem.
  • "The compiler will find them." It finds shape changes to typed things in one codebase. It finds nothing in templates, exports, other services, stored event names or the provider's records.

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.

  • GENERALValues, shapes, rules and boundaries propagate in every kind of system; the classification changes the search, not the move. In a data pipeline the readers are downstream jobs; in a library they are callers.
  • SCALE-SPECIFICIn a single codebase with strong types, shape changes are nearly free and the move concentrates on rules and out-of-repo readers. Across several services and teams, every hop is a deploy and a conversation, and the reader list becomes a coordination plan rather than a checklist.
  • ILLUSTRATIVEThe three tickets, the nine readers and the prefix that reaches the payment provider are invented to show how each class of change travels; the reader counts are plausible, not measured.

Where the depth lives

This domain asks the question and hands the answer off by name.