RequirementsGENERALDOMAIN-SPECIFICCONTESTED

Requirements Are a Snapshot

You were handed today's version. Which parts of it are stable and which are volatile is not a product question — it is the design input that decides what you hide behind what.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

I cannot predict future requirements, so how is "what will change" supposed to be an input to a design I make today?

The requirement

"Customers on the Pro plan get 100 GB of storage and priority support." Six months later: three plans, storage as a purchasable add-on, an enterprise plan negotiated per contract, and a grandfathered tier that must keep its old limits forever.

The obvious build

Model what was asked for. if (plan === 'pro') limit = 100 is exactly the requirement, it is readable, and inventing an entitlement system for two plans is textbook over-engineering.

Why it breaks

The if is not the problem — the problem is that it appears in the upload path, the report, the admin screen and the marketing page, so "what is a Pro customer entitled to" has four authorities and they drift (Duplicate Knowledge).

How it breaks as requirements change
  • The if is not the problem — the problem is that it appears in the upload path, the report, the admin screen and the marketing page, so "what is a Pro customer entitled to" has four authorities and they drift (Duplicate Knowledge).
  • When the third plan arrives it is a small change. When the *add-on* arrives it is not, because entitlement stops being a function of the plan and becomes a function of the plan plus purchases — a change of shape, not of value (Change Amplification).
  • When the enterprise contract arrives, entitlement stops being derivable at all and becomes data. Every design that computed it from a constant now has to store it, which is a migration and a backfill.
  • When grandfathering arrives, entitlement becomes a function of the plan, the purchases, the contract *and the date the customer joined*, and any design that did not keep that date is stuck.
  • The instructive part: at every step the previous design was reasonable for what was known. The mistake was not building too little — it was failing to notice which part of the requirement was the volatile one.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • Existing customers' entitlements must not change when the plan model does — a pricing change must never silently downgrade someone.
  • Sales need to be able to agree a bespoke limit for an enterprise customer without an engineering release.
  • The plan is referenced by the billing provider, the app, the marketing site and an internal report.
Invariants
  • What a customer is entitled to at any moment is a single, answerable question with one authority.
  • A customer's entitlement never changes as a side effect of a change to the plan catalogue.
  • Every entitlement change is attributable to a decision — a purchase, a contract, a migration — and not to a deploy.

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • One unit owns "what is this customer entitled to, right now" and answers it for every caller. It is the only thing allowed to know how entitlement is derived.
  • The plan catalogue owns what the plans currently are, which is a different and much more volatile thing than what a given customer has.
  • Nothing outside the entitlement owner may branch on a plan name. That rule is the whole design, and it is enforceable by a lint rule long before it is enforceable by a type (What to Automate Out of Review).
Boundaries
  • The boundary goes around the volatile part, and identifying it is the entire exercise: "how entitlement is computed" is volatile; "a customer has entitlements" is stable. Hide the first behind an interface shaped by the second (Information Hiding).
  • Do not draw the boundary around "plans". Plans are the thing that will stop being the answer, and a boundary named after a volatile concept has to be renamed when it changes (Naming and Domain Language).
  • The interface should express the question callers ask — "may this customer upload another 2 GB?" — not the data it happens to be computed from today (Designing a Module Interface).

The path a business rule walks

Requirements do not change randomly. In business software they move along a fairly consistent path, and knowing the path is most of what "designing for change" can honestly mean.

Each step is cheap if the one before it left a seam, and expensive if it did not. Notice that the expensive transitions are the ones where the *shape* changes, not the ones where the value does.

A constant becomes history
  1. 1
    A constant

    "Pro gets 100 GB." One number, written wherever it is needed.

    fails by Being written in eight places, so the first change is a search rather than an edit.

  2. 2
    A variable by category

    Three plans, three numbers. Still derivable from one field.

    fails by The if chain spreading into SQL and templates where no compiler can see it.

  3. 3
    A per-customer value

    Add-ons and enterprise contracts. Entitlement is now stored, not computed.

    fails by Every site that computed it from the plan is now wrong, and there is no list of them.

  4. 4
    A time-varying value

    Trials, promotions, grandfathering. The answer depends on when you ask.

    fails by The join date and the original plan were never stored, so the past cannot be reconstructed (Data Migration).

  5. 5
    A value with history

    Finance and support need to know what it was in March, and why.

    fails by In-place updates. The current value is knowable and no previous one is (Explicit State).

The two transitions that hurt are second-to-third and fourth-to-fifth, because both change what kind of thing entitlement is. A design that anticipated nothing else but kept the join date and avoided in-place updates would have survived all five.

Pricing the volatility bet

The argument for a boundary is only honest if it prices the change under both designs and admits what the boundary costs. Here is that comparison for the change that actually arrived.

Storage becomes a purchasable add-on
The change

Customers can buy additional storage in 50 GB blocks, on any plan, and enterprise customers may have a negotiated limit that ignores both.

Plan name branched on wherever entitlement matters
UploadServiceQuotaBannerAdminCustomerViewBillingSyncUsageReport (SQL)MarketingPricingPageOnboardingEmailSupportTooling
testsupload_testquota_testadmin_testbilling_sync_testreport_testonboarding_test
8 modules · 6 test files

Eight sites, two of which encode the limit in SQL and one in a template, so neither the compiler nor a rename will find them. The expensive part is not the edit — it is establishing that the list is complete, which under this design is not establishable.

One entitlement owner; callers ask "may this customer store 2 GB more?"
Entitlements
testsentitlements_testupload_integration_test
1 module · 2 test files

Callers are unchanged because they were never asking about plans. The add-on is a new input to one function, and the enterprise override is a stored value read by the same function.

what it cost The entitlement owner sits on the hottest read path in the product, so a call that was a field access is now a function call and, once overrides are stored, a database read — which pushed a caching decision into the design that did not exist before. It is also a coordination point: three teams now change the same module, and the marketing page no longer renders limits from a constant it controls, so a copy change needs an engineering release. That last one is a genuine regression, and the honest response is that it is worth it and it is still a regression.

Finding volatility without predicting the future

Volatility is observable. The last two years of changes to this area of the codebase are a record of what actually moves, and it is far better evidence than anyone's roadmap.

The practical version takes twenty minutes: look at what has been edited repeatedly, ask what the edits had in common, and check whether the thing they had in common has a single home. If it does not, you have found the boundary without predicting anything.

  • Look at the git history of the area, not at the roadmap. Files edited in six of the last ten releases are telling you where the volatility is (Shotgun Surgery).
  • Ask what the edits had in common. If they all changed the same rule in different places, that rule wants one home (Duplicate Knowledge).
  • Ask what sales are already promising. Bespoke terms agreed today are next quarter's requirement, and they are usually knowable now.
  • Separate the number from the shape. A number changing is cheap under any design; the relationship changing is what costs, and it is what to watch for.
  • Keep inputs you cannot recompute — join dates, effective dates, the version of the rule that applied — because those are the ones no future design can recover (Data Migration).
  • Write the volatility assumption down, so that when it turns out to be wrong the design can be revisited rather than defended (Revisit Triggers).
Stable question, volatile answer
changes quarterlyarrived laterarrived later stillonly possible because it was keptPlan cataloguePurchased add-onsNegotiated overridesJoin date + original planUpload / banner / report / adminMay this customer store 2 GB more?Entitlements (stable interface)
UserLLMAgentToolDataDecisionHumanGuardrail

How to build it

Most important first.

  • Separate the requirement into what is being asked for and what is being assumed. "Pro gets 100 GB" is a value; "entitlement is a property of the plan" is an assumption, and it is the assumption that will fail (Design for the Known, Name What You Assumed).
  • Rank the parts by volatility rather than by importance. Numbers change weekly, categories change quarterly, and the shape of the relationship changes rarely but expensively — and that is the ranking that decides what to hide.
  • Hide the volatile part behind an interface phrased in stable terms. This is the operational meaning of information hiding, and it is a different instruction from "add an interface".
  • Keep the inputs even when you do not use them. Storing the customer's join date and the plan they joined on costs two columns and is the difference between grandfathering being a config change and being an archaeology project.
  • Do not build the general mechanism yet. One if behind the right interface is correct; a rules engine behind the right interface is the thing this domain warns about (Speculative Generality).

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • Under the scattered-if design: adding an add-on costs finding every branch on plan name (typically eight to fifteen, several in SQL and one in a template), deciding for each whether add-ons apply there, and having no way to verify you found them all. Two to three weeks, and the residual risk is permanent — you will discover the sixteenth site through a support ticket.
  • Under the entitlement-owner design: adding an add-on costs one change inside one module and one new test. Callers do not change, because they were already asking the right question. Half a day. Adding the enterprise contract costs a stored override and one branch in the same module — a day. Grandfathering costs a date comparison, provided the join date was kept.
  • The change that stays expensive under both: making entitlement time-boxed. That adds a parameter to the question every caller asks, so every call site changes regardless of how well the module is bounded. This is the honest limit — a good boundary makes changes *within* the question cheap and does nothing for changes *to* the question (Stable Boundaries).
  • What the boundary cost: a level of indirection on the hottest read in the product, a module every team now queues behind, and the ongoing enforcement work of stopping plan names leaking back out.
What the recommended approach costs
  • Deciding what is volatile is a prediction, and predictions are wrong. This lesson's advice, applied confidently and repeatedly, produces exactly the speculative structure the complexity module argues against.
  • The interface shaped by the stable question is less convenient than the one shaped by the data, and every caller that wanted just the number pays for that.
  • Keeping inputs "in case" — join dates, original plans, effective dates — is storage and privacy surface for a requirement that may not come, and retention law does not care that you were being careful (Sensitive State).

What can go wrong

Failure modes
  • The volatile part is correctly identified and the interface is still shaped by the implementation — getPlanLimits(plan) rather than entitlements(customer) — so the abstraction leaks the very thing it was meant to hide (Leaky Abstractions).
  • The boundary is enforced by convention, so the fourth new engineer writes if (plan === 'enterprise') in a report and nobody notices for a year.
  • Volatility is guessed wrong in the other direction: the team predicts that storage limits will vary per customer and builds for it, and three years later there are still two plans. That is the cost of this lesson being applied without evidence (Premature Abstraction).
  • The design absorbs plans, add-ons and contracts beautifully, and then the requirement changes in a direction nobody modelled — entitlement becomes time-boxed, with trials that expire — and the interface has no place for "until when". Every volatility bet is a bet on an axis, and this is what losing on a different axis looks like (Choosing the Model).
Dependencies, and their direction
  • Everything that gates on entitlement depends on the entitlement owner — a deliberate, high fan-in, and the reason it must have no volatile dependencies of its own (Fan-in and Fan-out).
  • The entitlement owner depends on the customer record and on time, and time must be injected or grandfathering cannot be tested (Time as a Dependency).
  • It must not depend on the billing provider. The provider's plan ids are its vocabulary, and letting them in makes a provider change into a domain change (Anti-Corruption Layer).
Misreads
  • "So make everything configurable." Configuration is a volatility bet too, and a worse one: it moves the rule out of code, out of review and out of tests, into a place where it changes without a deploy and without evidence (Feature Flags and What They Cost).
  • "We should have built the entitlement system on day one." Probably not. Building the *interface* on day one is nearly free; building the mechanism is the expensive part and it should wait for the second case (The Rule of Three).
  • "You cannot predict requirements, so this is unfalsifiable." Volatility is not predicted, it is observed — from the last two years of tickets in this area, from the roadmap, and from what the sales team is already promising (The Cost of Change).
  • "Stable means never changes." Stable means changes more slowly than the thing you are hiding behind it. A boundary only has to outlive what it contains (Stable Dependencies).
Smells this explains
  • shotgun-surgery
  • divergent-change

Testing it, and how it ages

What to test, and at which boundary
  • Test entitlement as a pure function of customer, purchases and time. If a grandfathering test needs the system clock, injection was skipped and the test will fail in eleven months (A Deterministic Core).
  • Test the enforcement, not just the computation: assert that no code outside the module references a plan name, as a lint rule or a test over the source. Boundaries maintained by good intentions do not stay maintained (Dependency Cycles shows the same trick applied to direction).
  • Characterize the existing behaviour of every gate before moving it, because "Pro gets 100 GB" is stated and the fourteen sites are what is actually true (Characterization Tests).
How this design ages
  • Requirements change in a pattern that is worth learning: a constant becomes a variable, a variable becomes a per-customer value, a per-customer value becomes time-varying, and a time-varying value acquires history. Most business rules walk that path, and the steps are individually cheap only if the previous one left a seam.
  • The stable part is almost always a *question someone asks*, and the volatile part is almost always *how it is answered*. That heuristic is not universal but it is a better default than any other in this lesson.
  • The design stops fitting when the question itself changes. That is the signal to move the boundary rather than to add another parameter (Stability and Dependency Direction).

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • GENERALThat structure should hide what changes fastest behind what changes slowest is an observation about the cost of finding and editing code, so it survives any language or paradigm; only the mechanism for hiding differs.
  • DOMAIN-SPECIFICHow much volatility there is to hide is a property of the business, not of the code. Pricing, entitlement, tax and compliance rules churn constantly; a physics calculation or a file format parser does not, and applying this lesson there produces indirection with nothing behind it.
  • CONTESTEDThe strongest opposing case: volatility judgements are guesses dressed as analysis, and the empirical record of teams predicting which parts of a system would change is poor — so the more reliable strategy is to keep everything simple and direct, and pay the cost of the change when it actually arrives, having by then learned its real shape. That is a serious position and it wins whenever the guess is wrong. The counter is narrow and worth stating precisely: it is not about predicting *what* will change but about noticing which parts have *already* changed repeatedly, which is evidence rather than prophecy.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • System Design — the same volatility question decides which parts of a system get their own release cadence, where the unit of change is a deployable rather than a module.