Skip to content
STN New Delhi · Building in the open Filing · 0%

A new variant is not an additive change

The shared type can accept a new case while validation, storage, policy, and presentation continue enforcing the old system.

Argument Belief state Argument A defended position, written to be argued with. Mode · implementation Mode implementation A trail from inside a real system, architecture, or decision. architecture Theme architecture Primitives, boundaries, implementation shape, and the consequences of choosing one structure over another. maintenance Theme maintenance Care after construction, signal in hidden work, and cleanliness without the added drama.
Filed 11 min read

Conditions · green build, widening front

Some changes arrive looking like clerical work.

Consider this, you have a shared type that names the payment methods a system accepts: card, bank_transfer, and now you want to add another payment method, gift_card. It seems like it’s just one more value in a shared list. The compiler objects in a few predictable places: a missing label, an incomplete switch, a test still expecting the old set. As soon as each complaint is answered, the build turns green.

By every familiar signal, the system has accepted the change.

Then, an actual transaction enters it.

The first failure is almost reassuring. The validator rejects gift_card because its allowlist still names only the earlier methods. After adding the missing value, run the request again, and the system finally accepts it.

Buuuuut, It does not keep it.

The writer persists only the fields it already knows. Repair that, and the gift card reaches storage. But as soon as you read it back through a narrower projection, it fucking disappears again. Widen the read, and the policy layer quietly treats it as an ordinary card. Correct the policy, and analytics still calls it unknown. The interface gives it a fallback label that says nothing useful.

Each repair works. It also allows the new variant to travel one boundary farther into a system that has not met it yet.

The gift card is only a clean name for the pattern. The pressure arrived through new principals whose shared definition widened before storage and lifecycle had somewhere to put them; settings that passed validation and then disappeared inside an explicit writer; capabilities that became valid in the common catalogue while an older policy branch continued rejecting them.

Different nouns, same event. The vocabulary moved but the surrounding behavior remained where it was.

Nothing in those parts is obviously broken. Each one is still enforcing the system it was built to understand.

The shared vocabulary widened. The implemented system did not.

The syntax of adding a variant is additive. The architecture is not.

The green build

A compiler is very good at finding disagreements that have been expressed in a form it can see. Widen a closed type and every exhaustive switch can announce itself. Change a required shape and callers that depend on the old one may stop building. These failures are useful. They reveal part of the work.

But they do not reveal all of it.

A validator with an explicit list remains valid code. A writer with an explicit field list still writes exactly what it promises to write. A query selecting an old set of columns continues to return that set. A default branch continues handling unfamiliar input exactly as its author intended. A copied type in a different package may never learn that the canonical one changed.

Every local component can remain coherent while the system itself disagrees with itself.

That is why a green build creates a peculiar kind of confidence here. It proves that the visible disagreements have been repaired. It cannot prove that every behavioral boundary has made a decision about the new case.

The mistake is not using the compiler. It is allowing the compiler to define the work. A familiar shipping reflex widens the canonical declaration, repairs whatever becomes visibly inconsistent, and treats the end of compiler disagreement as the end of integration. A Disagreement Test Referenced concept The Disagreement Test Verification earns confidence by containing a differently sourced constraint capable of disagreeing with the implementation. helps identify the boundaries that have not been updated.

The compiler can expose the coupling but it cannot decide what a silent boundary should mean.

A build failure is one consumer announcing itself. Silence does not mean the others are ready.

What the one-line edit actually opened

A variant begins as a candidate case presented to an established seam: a type, schema, validator, registry, or handler boundary that already has an explicit disposition for every case within its scope. That is decision completeness. It does not mean the seam accepts every case; deliberate rejection is also a complete decision.

When that seam admits the candidate, it widens what the system officially considers valid. But the admission does not automatically widen every boundary built against the earlier set. Each affected boundary was decision-complete for the old vocabulary. Its treatment of the new case remains incomplete until it explicitly adopts, rejects, translates, or separates it.

This is why the declaration can change in one line while the architecture cannot. The new member does not merely join a list. It reopens the completeness claim carried by every affected seam.

Adding a variant therefore changes more than the set of values the system can name. It can change what the system is willing to accept, what it must remember, how it acts, and what it tells people afterward.

The work spreads according to meaning, not according to the size of the edit.

A new display label may stop at presentation. A new payment method can reach validation, fees, refunds, settlement, reporting, storage, and external contracts. A new principal can alter identity, authority, lifecycle, and the shape of records that were previously designed around one kind of actor. A new capability can be valid at the type level and still be denied by policy written for yesterday’s catalogue.

The first mistake is to estimate this work from the declaration that changed. The declaration shows where the new word entered. It does not show how far its meaning travels.

I call that distance the semantic reach of the change.

Semantic reach explains why some variant additions are nearly mechanical and others are migrations. The relevant question is not how many files contain the name. It is how many parts of the system assign meaning to the thing being named.

Once that reach crosses a persistence boundary, authority, stake, lifecycle, or an external contract, the change is no longer well described as adding one member to a list. It is a behavioral migration across a widening front.

Every repeated truth owes an answer

The search usually finds several versions of the concept. It is tempting to treat every repetition as a mistake and try to centralize it all. That makes the audit look decisive. It can also erase boundaries that were doing real work.

The classification has an order.

I have built a mental model for myself whenever I have to work on such problems to figure out what each representation is doing and to decide how to classify the occurrence.

In my experience, you should begin with understanding the meaning behind the seam and its implementation before you try to extend it. If the occurrence represents a genuinely different concept, classify it as a different model and preserve the separation. A gift_card may share a checkout surface with a bank_transfer while requiring different storage, settlement, or lifecycle. Forcing both through one convenient representation can hide the very semantics the new variant introduced.

If it represents the same concept, ask whether it follows an explicit owner. A label map, a generated schema, or a registry that follows the canonical catalogue is a derived representation. Verify that the derivation carries the new case or filters it by an explicit rule; do not add another manual synchronization point.

If the representation is independent, ask whether the difference is intentional. An API validator, a storage adapter, or a policy gate may deliberately accept only part of the wider domain. That makes it a deliberate boundary, not a stale copy. It must now decide whether to adopt, reject, translate, or separate the new case. Continuing silently is not a decision.

If the representation describes the same concept, follows no explicit owner, and has no deliberate reason to differ, it is an accidental private copy. It looks authoritative inside its own package, but static checking cannot protect two declarations that do not know they are related. These copies should usually converge on the canonical definition or be derived from it.

The convention is not to centralize everything. It is to stop treating silence as compatibility.

The variant extension audit

The audit begins at the canonical definition, but it does not end with its callers. It runs in two passes: first map where the concept is represented and make every representation declare why it exists, then follow the new case through the complete behavioral path and prove that its meaning survives. Its shape is a loop feeding a corridor:

VARIANT EXTENSION AUDIT

Follow the meaning, not the edit

Pass one loops until every representation has a role. Pass two walks the corridor and proves the case survives it. The lists below carry the instructions.

PASS 1 · MAP & CLASSIFY

  1. Map every representation
  2. Classify each one
  3. Widen the canonical seam
  4. Decide at each boundary

Every consumer the compiler exposes folds back into the map. Classify it before repair.

PASS 2 · TRACE THE BEHAVIORAL PATH

  1. Accept
  2. Store
  3. Read
  4. Act
  5. Present

Enter the case at Accept and prove it survives to Present. Then enter through later surfaces and inverse orders. Record the rationale and the propagation path in the same change.

The first pass owes each occurrence a role before it owes it a repair.

  1. Map without widening. Locate the canonical definition, then search for every consumer and every independent representation of the concept. Look beyond matching type names: validators, schemas, storage shapes, projections, policy branches, registries, handlers, tests, analytics, and presentation maps can all encode the old set through their behavior.
  2. Classify before changing. Give each occurrence its role from the classification above: different model, derived representation, deliberate boundary, or accidental private copy. The response is not always inclusion — a boundary may reject the variant, a transport may translate it, a different lifecycle may need separate storage.
  3. Widen, then fold. Only now add the variant where the system officially names the concept. Let the compiler expose consumers coupled closely enough to complain, fold each newly visible consumer into the map, and classify it before repairing it. A compiler failure reveals another obligation; it does not decide how that obligation should be met.
  4. Decide explicitly. At every deliberate boundary, record one of four answers: adopt, reject, translate, or separate. The dangerous answer is the one left implicit by old code.

The second pass begins once every representation has an owner and an explicit role. Now trace the complete behavioral path:

  1. Accept: Can valid input enter through every intended surface, and is it rejected clearly everywhere else?
  2. Store: Does persistence preserve the new case and the data that gives it meaning?
  3. Read: Do full and narrow projections return it without dropping or flattening it?
  4. Act: Do policy, lifecycle, authorization, and domain behavior make the intended decision?
  5. Present: Can interfaces, events, reports, and analytics describe what happened without falling back to the old world?

A happy path can show that the variant works. It cannot show that an older writer, reader, or policy branch will not erase its meaning afterward.

Record the reasoning in the same change as the code. If two models remain separate, explain why. If one boundary rejects the new case, make the rejection a part of its contract. If the change exposed an accidental copy, remove that copy rather than adding one more synchronized edit to its history.

This is not simply a checklist for running an audit on your own system. It is an algorithmic approach for following meaning through any system: it makes you a better author, and it leaves you with a better understanding of the system itself. The files will change as the architecture moves; the method survives them.

Build the corridor before the next arrival

Brownfield work discovers the integration path by searching for it. When variant growth is a known property of a greenfield system, that path can be made visible in advance.

Keep one canonical catalogue of available variants. Define the behavior each variant handler must supply. Prefer meaningful capabilities over scattered checks against type names. Derive validators and presentation registries where the boundary truly shares the complete catalogue. Run the same contract tests against every registered implementation. Document the boundaries that must remain narrower or cannot be derived.

This does not make a new variant free. A gift card still needs gift-card semantics. The corridor simply gives those semantics known addresses. Instead of rediscovering validators, handlers, labels, and contracts across the system, the work arrives at an integration seam designed to expose what is missing.

A good extension seam makes the cost visible, bounded, and difficult to pay incorrectly.

That last condition matters. A registry that accepts a name but does not force behavior is only a more polished version of the original problem. The extension point must demand the decisions the system needs, not merely provide somewhere to list the new member.

Not every dropdown is a migration

The discipline has to remain proportional.

Some changes really are a label, a color, or a display-only option. Turning each one into an architectural ceremony would hide small work beneath a method designed for large work. Centralization can also erase useful ownership. Generic storage can push meaningful differences into unvalidated data. A plugin-shaped extension corridor can become speculative machinery for variants that may never arrive.

Use the full audit when the variant changes identity, money, authority, persistence, lifecycle, external contracts, or user-visible behavior whose meaning depends on state or policy outside presentation. For a purely presentational addition, follow only the surfaces inside its actual semantic reach.

The goal is not to make every addition expensive. The goal is to prevent a cheap declaration from concealing expensive meaning.

The compiler did its job. It reported the disagreements it could see. The remaining work belonged to the system that stayed silent.

A declaration can make a new case legal. Only integration teaches every affected boundary what that legality requires.

Until then, the build is green. But, is the variant integrated?

You can’t build a corridor for a variant that doesn’t exist.

More from this theme