AbstractionGENERALDOMAIN-SPECIFICCONTESTED

Choosing the Model

An abstraction buys flexibility along one axis and pays along every other. Which axis you pick is not a detail of the design — it is the design.

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

Given that any abstraction makes one kind of change cheap and other kinds more expensive, how do I choose which kind?

The requirement

A pricing engine must handle per-customer discounts, regional tax, promotional campaigns and a new subscription model. Four engineers propose four abstractions and each one is right about the change it anticipates.

The obvious build

Pick the most flexible design. A rules engine handles everything: rules are data, new pricing behaviour needs no deployment, and no future requirement is excluded. It is the option that appears to dominate all the others, which is exactly why it gets chosen.

Why it breaks

Maximum flexibility along one axis is maximum rigidity along the others. A rules engine makes "add a rule" trivial and makes "change what a rule *is*" — adding a new kind of condition, or ordering semantics — a change to an interpreter with every existing rule as its input (Extensibility).

How it breaks as requirements change
  • Maximum flexibility along one axis is maximum rigidity along the others. A rules engine makes "add a rule" trivial and makes "change what a rule *is*" — adding a new kind of condition, or ordering semantics — a change to an interpreter with every existing rule as its input (Extensibility).
  • It moves logic out of the language and into data, so the type system, the tests and the debugger stop helping, and "why was I charged this" becomes an interpreter-tracing exercise (Debuggability by Design).
  • "Handles everything" is unfalsifiable, and an unfalsifiable claim about flexibility is the standard justification for the most expensive structures in a codebase (Speculative Generality).
  • The axis it optimises — non-engineers changing pricing without deploys — is often not even wanted. If pricing changes need finance approval and an audit trail anyway, the deploy was never the bottleneck (The Cost of Change).
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
  • The roadmap is credible for two quarters and speculative beyond that.
  • Pricing is edited roughly weekly, so whichever model is chosen will be exercised hard and soon (Stability and Dependency Direction).
  • Whatever is built has to be explicable to a finance analyst, because they are the ones who will find the discrepancies.
Invariants

Who owns what, and where the seams fall

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

Responsibilities
  • The model owns exactly one kind of variation, named. "Pricing is a sequence of adjustments" is a model; "pricing is flexible" is not (What an Abstraction Actually Is).
  • Whoever proposes it owns saying which change it makes *harder*, because that sentence is what turns a pitch into a design proposal.
  • The team owns choosing based on the two credible quarters rather than on the speculative ones, and recording the trigger that would change the choice (Revisit Triggers).
Boundaries
  • The axis of variation is the boundary. Everything that varies along it is data or a plug-in point; everything else is code and changing it is a code change (Plugin Architecture).
  • Getting the axis wrong is much worse than getting the mechanism wrong. A clumsy implementation of the right model is fixable; an elegant implementation of the wrong one has to be replaced (Premature Abstraction).
  • The axis should be chosen from observed variation — what has actually changed in the last year — not from the imagined space of what could (Finding Seams).

The same requirement, four axes

Four engineers propose four abstractions for pricing. Each is a sensible response to a real kind of change, and the disagreement is not about code quality — it is about which kind of change is coming.

Draw them as axes and the choice becomes discussable. The question stops being "which design is better" and becomes "which of these four kinds of change did we receive most of last year", which has an answer sitting in the issue tracker.

  • Every arrow has a label on both ends in practice — the cheap change and the expensive one — and a proposal that only names the first is incomplete (The Trade-off Matrix).
  • The four are not ordered by sophistication. The rate table is the simplest and it is the correct answer for a business whose pricing genuinely varies only by region (KISS: Simplest for the Requirements You Have).
  • Choosing two of them at once is the failure worth naming: a rules engine *and* a strategy hierarchy means every change touches both (Over-Decomposition).
Four models, four axes of cheap change
expensive: per-contract formulasexpensive: rules crossing categoriesexpensive: a new kind of conditionexpensive: anything conditionalPricing must absorb changeOrdered adjustment listStrategy per customer typeData-driven rules engineRate table per regionCheap: new discount, new taxCheap: new customer categoryCheap: new rule without deployCheap: new region, new rate
UserLLMAgentToolDataDecisionHumanGuardrail

Comparing the models honestly

Scores make the shapes comparable and imply a precision that does not exist. They are useful for one thing: noticing that no column is dominated, which is what tells you this is a choice rather than a ranking.

The row worth reading twice is the last. A rules engine is not the sophisticated version of the others; it is a different bet with a much worse debuggability profile, and it is correct exactly when its axis is the real one.

Four pricing models, for a business with weekly pricing changes
OptionSimplicityFlexibilityTestabilityOperationalMigration costNote
Ordered adjustment listPrice is a base plus a sequence of named adjustments. Cheap for new discounts and taxes, and it satisfies the explainability requirement for free because the sequence *is* the explanation. Expensive when pricing stops being a sequence — per-contract formulas, or adjustments whose order depends on each other.
Strategy per customer typeOne implementation per customer category. Cheap for a new category, and readable. Expensive for any rule that crosses categories, which is where it fails: regional tax applies to all of them, so it either duplicates into every strategy or leaks into a shared base (Composition Over Inheritance).
Data-driven rules engineRules as data, evaluated by an interpreter. Genuinely cheap for a new rule with no deploy, which is a real requirement in some organisations. Expensive for a new *kind* of condition, and it removes the type system, the debugger and ordinary tests from the picture — the rules now need their own CI (Validate at Startup, Fail Clearly).
Rate table per regionA lookup table. Trivial, fast, obvious, and completely unable to express anything conditional. The right answer for a genuinely simple pricing domain and a trap if conditionality is arriving (YAGNI, With Its Bill Attached).

caveat These numbers compare shapes for one business with weekly pricing changes and a hard explainability requirement; they are an Engineer Atlas model rather than a measurement, and they cannot express the variable that usually decides — who changes pricing. If it is engineers, the adjustment list wins comfortably. If it is a pricing team that must ship between deploys with an audit trail, the rules engine's poor scores here are simply the price of the only option that meets the requirement, and calling it over-engineering would be wrong. The axes also hide that "flexibility" means different things per row: the rules engine is flexible about rules and rigid about rule *kinds*, which no single number can carry.

When the requirement lands on the wrong axis

The value of naming the axis is not that it prevents the awkward requirement. It is that when the awkward requirement arrives, the team recognises what has happened instead of concluding that the code is bad.

Below, the same requirement is priced under a model aligned with it and one that is not. The point of the comparison is the cost line: the aligned model bought that cheapness by giving something up, and what it gave up is exactly what the second requirement needed.

Per-contract negotiated pricing
The change

Enterprise customers get a negotiated price computed by a formula specific to their contract, which may reference volume tiers, committed spend and a custom floor.

Ordered adjustment list — the model chosen for discounts and taxes
Pricing model types (a new non-sequential concept)every adjustment implementationthe price breakdown formatstored breakdowns migrationadmin explainer UIfinance export
testsMost pricing tests, because the shape of a price changeda data migration test
6 modules · 2 test files

A negotiated formula is not an adjustment applied in sequence to a base, so it does not fit the model at all. The options are to force it in as a terminal "override" adjustment — which quietly breaks the explainability invariant, since the breakdown no longer explains anything — or to widen the model, which touches every caller and every stored breakdown.

A model whose axis is "price is computed by a per-customer pricing policy"
a new PricingPolicy implementationcontract configuration
testsenterprise_policy_test
2 modules · 1 test file

One implementation of an existing concept. The requirement lands along the axis this model was chosen for, and nothing else moves.

what it cost This is not an argument for the second model — it is an argument for knowing the axis. The policy-per-customer model pays heavily for its win here: a new tax rule now has to be replicated into or shared across every policy implementation, exactly the "rule that crosses categories" problem that made the strategy model unattractive, and the free explainability of an ordered adjustment list is gone because a policy is a black box that returns a number. If last year's tickets were mostly taxes and discounts and this contract requirement is the first of its kind, the adjustment list was the right choice and absorbing this one badly is the correct outcome. Choosing an axis means accepting that some requirement will land across it; the failure is not planning for it, it is not noticing which one you chose (Decision Records).

How to build it

Most important first.

  • List the changes you have actually received. Twelve months of pricing tickets, sorted by what kind of change each one was. The dominant kind is your axis, and it is usually not the one being argued about.
  • For each candidate model, write the sentence "this makes X cheap and Y expensive" with real examples in both slots. A candidate whose Y is empty has not been understood (The Trade-off Matrix).
  • Prefer the model that matches the two credible quarters, not the one that covers the most imagined futures (Design for the Known, Name What You Assumed).
  • Prefer models that keep behaviour in code where the language can check it, unless the axis genuinely is "non-engineers change this without a deploy" — that is a real requirement and a rare one (Making Illegal States Unrepresentable).
  • Make the explainability requirement part of the model rather than an afterthought: if every price must be traceable, the model is "an ordered list of adjustments" and that decision is already made (Stable Identifiers).
  • Write down the axis and the trigger that would move it. In eighteen months the choice will look obvious or stupid, and in both cases nobody will remember what was known (Architecture Decision Records).

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
  • Aligned with the axis: a new regional tax rule is a new adjustment type and a test — hours. This is the case the model was chosen for and it should feel almost boring.
  • Across the axis: "prices are now negotiated per contract, computed by a customer-specific formula" is not an adjustment in a sequence. Under an adjustment-list model, that is a new concept, a new interface for callers, and a migration of stored price breakdowns — weeks, and the model is a hindrance rather than a help.
  • The number worth knowing before committing: what fraction of last year's pricing tickets would have been "aligned" under this model. If it is under half, the axis is wrong and no amount of implementation quality will rescue it (Change Amplification).
What the recommended approach costs
  • Choosing one axis explicitly means saying out loud that another kind of change will be expensive, which is politically harder than promising flexibility.
  • Evidence-based axis selection is backward-looking, and after a strategy change the last twelve months may be the wrong sample.
  • A simpler model that matches the axis will look less impressive than a general mechanism that does not, and that asymmetry biases design discussions toward the wrong answer (Simple Is Not Easy).

What can go wrong

Failure modes
  • The axis is chosen from the loudest recent incident rather than from the distribution of changes, so the model optimises for something that happened once (Correlation Is Not the Root Cause).
  • Two axes are chosen at once — a rules engine *and* a strategy hierarchy — producing a system where every change requires touching both mechanisms (Over-Decomposition).
  • The model is right and the vocabulary is wrong, so finance and engineering use the same words for different things and reconciliation becomes a standing meeting (Ubiquitous Language).
  • The mitigation fails in a predictable way: the ticket-sorting exercise is done once, at design time, and never repeated — so the axis stays fixed while the distribution of changes moves under it.
Dependencies, and their direction
  • Every caller depends on the model's vocabulary, so changing the axis later means changing the vocabulary everywhere — which is why this decision is much less reversible than the code volume suggests (Reversible and Irreversible Decisions).
  • A data-driven model creates a dependency on data that is now production behaviour, with the deployment, review and migration obligations of code and none of the tooling (A Config Change Is a Production Change).
  • A model expressed in types creates a dependency on the language, which is a feature: the compiler enforces the model at every call site (Interface Versus Implementation).
Misreads
  • "So pick the most flexible option." Flexibility is not a scalar. Every design is rigid somewhere, and a design that claims otherwise has simply not named where (Extensibility).
  • "Get the axis right and you are done." The axis is the largest decision, not the only one. Vocabulary, explainability and migration still have to be designed (Domain Modeling).
  • "This is over-thinking a pricing class." The class is not the cost. The vocabulary it imposes on every caller is, and that is what makes the choice expensive to reverse (API Stability).
  • "A rules engine is always over-engineering." Sometimes it is exactly right — when non-engineers genuinely must change behaviour between deploys, and the organisation has the review and audit process to make that safe (Plugin Architecture).
Smells this explains
  • speculative-generality
  • divergent-change

Testing it, and how it ages

What to test, and at which boundary
  • Test the axis, not the mechanism: write the two or three most likely upcoming changes as tests against the candidate model before committing to it. It takes an afternoon and it is the cheapest available evidence (Testing as Design Feedback).
  • Test explainability directly — a price breakdown that a human can read is a requirement, so assert it (Stable Identifiers).
  • For data-driven models, the data needs tests too, and they need to run in CI like code, because the data *is* behaviour (Validate at Startup, Fail Clearly).
How this design ages
  • Models age by axis drift: the model stays correct and the distribution of incoming changes moves, so the same design gets slowly worse without anything breaking (Evolvability).
  • Re-running the ticket-sorting exercise annually is the cheapest check available, and its output is a sentence rather than a project.
  • When the axis has genuinely moved, the honest response is usually a new model alongside the old with an incremental migration, not a rewrite (The Strangler Pattern).

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 flexibility along one axis is paid for along the others is a property of what an abstraction does — fix some things so others can vary — so it holds for a class hierarchy, a config schema, a plugin API and a database schema alike.
  • DOMAIN-SPECIFICIn pricing, insurance rating and tax, the axis is usually "new rules of a known shape", which is what makes data-driven models genuinely successful there. In workflow or approval systems the axis is usually "new steps and new orderings", which points at an explicit process model instead — the same reasoning, a different answer, and copying the pricing answer into a workflow system is a common and expensive mistake.
  • CONTESTEDThe strongest opposing view is that axis selection is a fiction: nobody can predict which kind of change will dominate, the last twelve months of tickets mostly reflect what the current design made easy rather than what the business wanted, and therefore the rational strategy is to keep the design as simple and as concrete as possible and rewrite the small piece when the requirement lands. That argument has real force — the sample genuinely is biased by the existing design, which is a subtle point most planning ignores. The reply is narrow: it is still better to name the axis you are betting on than to bet without noticing, and the bias in the sample is an argument for weighting rejected and deferred requests too, not for abandoning the evidence.

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 reasoning picks a partitioning key: it makes one access pattern cheap and every other one a scatter-gather, and choosing it is the design rather than a detail of it.
  • Programming Languages & Runtime Internals — a model expressed in types is checked at every call site, while one expressed in data is checked only where someone wrote a validator; that difference is what the rules-engine row is really paying for.