ComplexityGENERALSCALE-SPECIFICCONTESTED

The Complexity Budget

Every feature spends concepts, states, dependencies and failure modes out of a budget nobody is tracking. Tracking it does not make features cheaper — it makes the price visible while the decision is still open.

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

We keep saying yes to individually reasonable features and the system is getting hard to hold in one head. What are we actually spending, and how would we know we had run out?

The requirement

Product wants gift subscriptions: buy for someone else, redeem with a code, start on redemption, and refundable until redeemed. Everyone agrees it is a small feature.

The obvious build

It is one entity and a redemption endpoint. Two weeks. We have built four things like this.

Why it breaks

The estimate prices the code. What it does not price is that gifts multiply with everything already in the model: gift plus trial, gift plus coupon, gift plus pause, gift plus plan change, gift plus refund window. Those combinations are not new code; they are new states someone has to have thought about.

How it breaks as requirements change
  • The estimate prices the code. What it does not price is that gifts multiply with everything already in the model: gift plus trial, gift plus coupon, gift plus pause, gift plus plan change, gift plus refund window. Those combinations are not new code; they are new states someone has to have thought about.
  • It adds a new external dependency on the email provider for delivery, and a new failure mode — code generated, payment captured, email never delivered — that has no owner and no runbook.
  • It adds two new concepts to the vocabulary: purchaser and recipient are now different people, which means every place that said "the customer" now has a question to answer (Ubiquitous Language).
  • None of that shows up in the estimate, and none of it shows up in any single pull request. It shows up four features later, when a change to proration breaks gifts and nobody predicted the interaction because nobody had the whole model.
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 billing model already has plans, trials, coupons, proration and pauses; each was also a small feature.
  • Six engineers, and two of them joined this quarter.
  • On-call is the same six people, so every new failure mode is a page one of them will take at 3am.
Invariants
  • A gift is charged once, at purchase, and never again.
  • A gift code can be redeemed at most once.
  • A refunded gift can never be redeemed afterwards, and a redeemed gift can never be refunded through the gift path.

Who owns what, and where the seams fall

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

Responsibilities
  • Whoever accepts the feature owns accepting its budget line: the concepts, states, dependencies and failure modes it adds, listed before the estimate is given.
  • The subscription lifecycle owner decides whether gift is a new state or a new attribute, because that choice decides how many combinations exist.
  • On-call owns nothing new by default, which is the problem: new failure modes need an owner named at the time, not discovered during an incident (Failure-Aware Feature Design).
Boundaries
  • The budget is per model, not per repository. Two hundred small features in modules that do not interact cost far less than twenty in one lifecycle, because interaction is what the budget is denominated in (Local Reasoning).
  • A feature that can be contained behind a boundary — no new state on the core entity, no new combination — is genuinely cheap, and containment is worth designing for on exactly the features that would otherwise multiply.

The line item nobody writes down

The estimate says two weeks. The budget line says something different and takes ten minutes to produce: what does this feature add to the number of things that interact?

Four counts are enough. Concepts, because each one enters the vocabulary and every future reader carries it. States and their combinations, because that is where interactions live. Dependencies, because each is a thing that can be down. Failure modes, because each is a page at 3am with an owner or without one.

Gift subscriptions — the budget line, written before the estimate
1Feature: Gift subscriptions
2
3Concepts +2 purchaser vs recipient (previously one "customer")
4 redemption code as a thing with its own lifetime
5
6States +3 gifted-unredeemed, gifted-redeemed, gifted-refunded
7 x existing: trial, coupon, paused, plan-change
8 -> ~12 combinations that need an answer, of which
9 4 are currently undefined (gift + pause, gift + coupon,
10 gift + plan change, redemption during trial)
11
12Deps +1 email delivery on the purchase path (was async-only)
13
14Failures +2 payment captured, code never delivered -> no owner
15 code redeemed twice under concurrent requests -> needs
16 a uniqueness guarantee, not a check-then-write
17
18Removal none. No flag, no sunset. Permanent line item.

The four undefined combinations are the finding. They are not extra work discovered later — they are decisions that exist right now, unmade, and each will otherwise be made accidentally by whoever writes the code for it.

Where to put the spend

Once the line item exists, the design question is not whether to build the feature but which shape of it spends least on the term that grows fastest. Gift as a parallel lifecycle is the expensive shape; gift as an attribute of the existing lifecycle is the cheap one; gift as its own bounded module is expensive now and cheap later.

These are genuinely different bets and none of them is free. Scoring them makes the disagreement concrete, as long as everyone remembers that the scores are a way of writing down an opinion, not a measurement of anything.

Three shapes for gift subscriptions
OptionSimplicityFlexibilityTestabilityOperationalMigration costNote
Attribute on the existing subscription lifecycleOne lifecycle, one set of transitions, gift is a flag plus a recipient. Every later lifecycle change is one edit. It strains when gift rules genuinely diverge — a gift that never renews is already an awkward special case here.
Parallel gift lifecycle alongside subscriptionsReads well in isolation and matches how product talks about it. Every future lifecycle change is now reasoned about and tested twice, and the two drift because nothing forces them to agree.
Gift as its own module with a narrow contract to billingThe most expensive to build and the only one that keeps the billing model's budget flat as gifts get complicated. Worth it if gifting is a growing product area; over-design if it stays one endpoint (Over-Design and Under-Design).

caveat These numbers compare three sketches against today's requirements and cannot express the thing that actually decides: whether gifting grows. If product ships corporate bulk gifting next year, the third row is the only one that was ever right and the first row's migration score becomes irrelevant. Read the notes, not the digits — the digits carry no unit and nothing was measured.

Reviewing the budget, and paying some of it back

A budget that is only ever spent is not a budget. The repayment side is deletion, and deletion is the least-scheduled activity in software because it has no visible upside and a small chance of breaking something.

The most reliable repayment is removing optionality nobody exercised: the flag that has been true in every environment for two years, the configuration value nobody has ever set, the abstraction with one implementation. Each removal takes a state combination out of the model permanently.

A quarterly budget review that takes an afternoon
  1. 1
    List the flags

    Every feature flag, config switch and environment-dependent branch, with the date it was added.

    fails by Being run from the codebase and missing the ones that live in a dashboard, which are the ones nobody remembers (Feature Flags and What They Cost).

  2. 2
    Find the constants

    Which of them have had the same value everywhere for more than a quarter? Those are not options, they are decisions with extra states attached.

    fails by Treating "someone might want it" as evidence. Ask who, and when they last asked.

  3. 3
    Check the combinations

    For the states in the core model, list the pairs that are reachable and ask which have a defined answer.

    fails by Enumerating by hand and missing the ones that only occur under a race (Invalid Transitions).

  4. 4
    Delete one thing

    Remove a flag, a branch, or an unused extension point, with the tests to prove behaviour did not move.

    fails by Removing something that was load-bearing in a path nobody tested (Characterization Tests).

  5. 5
    Record what you did not delete

    For each thing kept, one line saying who wants it and what would let it go.

    fails by Producing a list nobody reads. It only works if it is short and attached to the next review (Revisit Triggers).

The review is worth doing badly. Deleting one flag a quarter is a rounding error on delivery and it is the only force in the codebase pushing the other way.

How to build it

Most important first.

  • Write the budget line before the estimate. Four counts: new concepts, new states or state combinations, new dependencies, new failure modes. It takes ten minutes and it is the only artefact that makes "small feature" checkable.
  • Attack combinations first, because they are the term that grows fastest. Making gift an attribute of an existing lifecycle rather than a parallel one is the difference between adding a case and multiplying the cases (State Machines).
  • Price removal at the same time. A feature with no removal path is a permanent line item, and the ones that never get removed are the ones nobody can prove are unused (Feature Flags and What They Cost).
  • Say no with the ledger rather than with taste. "This adds two concepts, six state combinations and a delivery failure mode we have no runbook for" is an argument product can engage with; "this feels complicated" is not.
  • Spend deliberately on the things that differentiate. A budget is not a case for building less — it is a case for knowing which features you are prepared to be complicated for (Build, Library, SaaS or Managed Service).

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
  • With gift as a parallel lifecycle: the next lifecycle change — say, "trials can be extended once" — must be reasoned about twice, once for ordinary subscriptions and once for gifts, and the tests double. Every subsequent lifecycle change pays this, permanently.
  • With gift as an attribute of one lifecycle: the same change is one edit plus one added test case for the gift variant. The cost of the next change stays roughly flat instead of doubling with each parallel path.
  • The budget line for a feature is not the cost of building it. It is the amount by which it raises the cost of every later change to the same model, which is why cheap-to-build and cheap-to-own are different numbers.
What the recommended approach costs
  • The ledger adds process to every feature, and process is exactly the thing that erodes first under delivery pressure.
  • Counting concepts and states is subjective. Two engineers will produce different numbers for the same feature, and the numbers are only useful as a prompt for the conversation, never as a measurement.
  • A team that takes the budget too seriously ships a simpler product than the market wanted, which is a failure mode with no code smell attached to it.

What can go wrong

Failure modes
  • The ledger becomes a form. Four numbers get filled in to satisfy the process, nobody argues about them, and the exercise costs ten minutes per feature and buys nothing.
  • The numbers get treated as a threshold — "we allow six new states per quarter" — which turns judgement into an arbitrary gate and invites gaming (Revisit Triggers).
  • The budget is used to block work rather than to shape it, so people stop mentioning what a feature really touches.
  • Complexity is counted only for new features, while the existing model keeps compounding untracked. Most of the budget in a five-year-old system was spent before this quarter (What Technical Debt Actually Is).
Dependencies, and their direction
  • Each feature adds dependencies in both directions: on infrastructure it needs, and from the rest of the model onto the states it introduced.
  • Dependencies added for one feature are load-bearing for all the later ones. The email provider added for gift delivery becomes something four features assume, and its failure mode is now shared (Volatile Dependencies).
  • The heaviest dependency is on human attention: every added concept is one more thing that has to be in the head of whoever reviews the next billing change (Bus Factor).
Misreads
  • "So we should build fewer features." The budget is about spending knowingly, not spending less. A product that wins by being complicated — a tax engine, a scheduler — is spending correctly.
  • "The budget is the story-point estimate." Points measure build cost. The budget measures what the feature does to the cost of every later change, and those two are frequently in opposite directions (The Cost of Change).
  • "Microservices give each team its own budget." Splitting the deployment does not split an entangled model; it adds network failure modes to it. The budget is denominated in interacting concepts, wherever they are deployed.
  • "We can score the codebase." Any single number here would be invented. The four counts are useful because they are concrete and local to one decision, not because they aggregate (What Makes Software Hard to Change).
Smells this explains
  • speculative-generality
  • boolean-flag-explosion

Testing it, and how it ages

What to test, and at which boundary
  • Test the combinations, not the feature. Gift plus coupon, gift plus refund, gift redeemed after pause — the interactions are the budget, so they are what the tests should be about.
  • A property test over the lifecycle that asserts the forbidden pairs can never both be true is cheaper than enumerating combinations by hand (Property-Based Testing).
  • Test the new failure mode explicitly: payment captured, email not delivered. If there is no test, there is no design for it, only a hope.
How this design ages
  • Budgets are spent permanently and repaid rarely. Deletion is the only real repayment, and it is the least-scheduled work in software (Speculative Generality).
  • The moment to notice you are over budget is when estimates stop correlating with feature size — when a two-day feature takes two weeks because of interactions nobody listed.
  • Eventually the model splits: gifts become their own bounded thing with its own lifecycle and a narrow contract to billing. That is the expensive repayment, and it is worth planning before it is forced (Extract Module).

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 interacting concepts multiply rather than add is a property of state spaces, so it holds regardless of stack — though a language with sum types makes the combinations visible at compile time, which changes how quickly you notice you are over budget.
  • SCALE-SPECIFICFor a team of six sharing one model, the binding constraint is what fits in a person's head, and the budget is real. At two hundred engineers across twenty owned modules the constraint moves to the contracts between them, and per-module budgets matter more than a global one (Internal Module Contracts).
  • CONTESTEDThe strongest opposing case is that this framing gives engineering a veto dressed up as accounting: the counts are subjective, the interactions are frequently over-predicted, and products that shipped complicated features early often beat the tidier competitor precisely because they were willing to absorb the mess. Held honestly, the ledger is an input to a product decision — the failure is when it becomes the decision.

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 budget in a distributed setting is denominated in services, queues and failure modes rather than concepts, and the multiplication is harsher because each interaction can also be partially completed.