Overview
Modifiability — also known as changeability — is the property of software that lets it accept change at low cost and low risk. Software — user interfaces in particular — never reaches a finished state: designs are refined, feature requirements shift, and data-handling needs evolve, so the practical measure of a codebase is not how well it works today but how cheaply and safely it absorbs the next change. Change arrives from many directions at once: design changes, feature-requirement changes, and data-processing concerns such as performance. A structure that resists change turns each of those changes into a large, risky project; a structure built for change turns most of them into small, routine edits. Building and maintaining that structure is a core part of development work, not an optional polish step. This page defines modifiability by classifying changes into three kinds — separate, revise, and remove — and explains why the classification matters, the three patterns that break modifiability, and how to protect it through collaboration across roles.The three kinds of change
Not every change stresses a codebase the same way. Classifying an incoming change by what stays the same and what is altered tells you which part of the structure the change relies on, and therefore what must be prepared in advance.
The following code — a list view for order history — serves as the running example for all three kinds.
OrderItem owns the representation, OrderList assembles the list, and OrderHistoryModal is the container; each is a self-contained unit that receives its data from outside.
Running example: an order history list
Separate
A change is a separation when the inputs and outputs of the affected units do not change — only their arrangement, presentation, or volume does. Moving content from a modal dialog to a dedicated page is a typical example. The data the content receives is the same, and the data it produces is the same; only the container changes. If the content is a self-contained unit that receives its data as input, the change amounts to relocating that unit — a matter of minutes, not days.Separate: from a modal to a page
Revise
A change is a revision when the representation of inputs or outputs changes: a card starts displaying an image, a form collects one more field, a list shows the newest entries first. A revision requires each affected unit to respond to the change. When units are separated along stable concepts, each response stays local: the unit that owns the changed representation adapts, and units that do not touch the changed representation stay untouched.Revise: add a product image
Static types
When types are derived from a single source of truth (SSOT), changing a definition surfaces every affected location as a type error, before the code ever runs.
Automated tests
Tests that assert observable behavior fail exactly where a revision alters behavior, turning “what did this break?” into a mechanical question. See Test Code.
A revision that fails to fail anything deserves suspicion: either the change had no observable effect, or the detection net has a hole. Noticing that a test should have failed — and fixing the test — is part of making revisions safe.
Remove
A change is a removal when a mechanism or its representation is deleted outright. Deleting code is easy; anyone can do it. The hard part is doing it safely, quickly, and simply — deleting one thing must not break an apparently unrelated part of the system. Whether a removal takes minutes or days of tracing hidden dependencies is decided long before the removal happens, by how the code was structured when it was written.Remove: retire the order history feature
Why the classification matters
The classification is useful because each kind of change relies on a different structural preparation, and because it turns “make the code easy to change” from a slogan into concrete design decisions. Each kind demands its own preparation. Separations rely on unit boundaries that follow the conceptual data model. Revisions rely on change detection — types and tests that surface the reach of a change. Removals rely on explicit, enumerable dependencies. A codebase can be strong in one and weak in another, so “is this easy to change?” is really three questions. Misclassification wastes effort in both directions. Treating a separation as a revision leads to rewriting logic that only needed relocating. Treating a revision as a separation ships a behavior change without updating the tests and types that should have tracked it. Naming the kind of change first keeps the response proportional. For example, the earlier “modal to page” change ends up looking quite different depending on how it is handled:Treating a separation as a revision
Re-declared vs. derived types
greeting later requires a new field. With the hand-copied type, MessageArgs silently keeps its old shape: only the internal call to greeting errors, the fix stays local, and callers of message never learn that the concept they depend on has changed.
With the derived type, the requirement propagates into MessageArgs, and every caller of message fails to compile — the full reach of the change is visible immediately.
The dependency exists either way; deriving the type is what makes it visible to the compiler, and therefore detectable.
Three patterns that break modifiability
Even a structure built for change has limits. When changing well-structured code still hurts, the cause is usually one of three patterns — and all three originate not in the code but in how changes are decided and communicated.Broken premises
A structure is built on an agreed conceptual model: which entities exist, how data is grouped, where the boundaries lie. A broken premise is a change that invalidates that model itself, or an implementation that violates the agreed decomposition after the fact. For example, suppose the agreement was “group the data by date, with each group holding entries of kind and value,” and the delivered data instead groups by kind, holding per-date values inside. Every unit boundary that was drawn from the agreed model is now drawn in the wrong place, and the cost is not one revision but a cascade of them. Conceptual models are the foundation that separations and revisions stand on. Aligning on them — and treating a change to the model itself as a consultation with room for pushback, rather than a notification — is what keeps the foundation stable.Inconsistency
Inconsistency is when the same kind of representation or the same data concept is specified differently from one place to the next, or when a specification flips back and forth — changed, reverted, then changed again. Each inconsistent variant needs its own structure and its own change-detection wiring, so the cost of the machinery that makes change cheap is paid repeatedly for what is conceptually one thing. Modifiability is a means of converging on a better product through iteration; it is not a mechanism for absorbing decisions that were never really made. The prevention is consistency and record-keeping: reuse the established pattern for an established concept, and when a decision is deliberately revisited, record what changed and why, so the codebase converges instead of oscillating.Unannounced changes
Humans are poor at detecting differences. Feature-level changes get examined from every angle — risk, feasibility, compatibility — but a small visual or behavioral change, such as centering a piece of text, feels risk-free to the person requesting it and slips through without being announced. The asymmetry has a structural cause: functional requirements have obvious failure modes, so they attract scrutiny; non-functional details do not, so they reach implementation without ever being examined. But an unannounced change is invisible to every safety net — no test is updated, no reviewer knows what to look for, and the change itself is recorded nowhere. The prevention is to make every change explicit: communicate it when requesting it, leave it in a written log, and describe it in the pull request that implements it, so the change is visible to reviewers and to anyone reading the history later.Keeping modifiability across roles
Modifiability cannot be maintained by the implementing team alone. The three breaking patterns all arise at the boundaries between roles — between design and implementation, between API providers and consumers, between the person requesting a change and the person making it. Protecting modifiability is therefore a collaboration protocol, not just a coding standard.1
Agree on concepts before implementation
Align on the conceptual data model — the entities and their grouping — before code is written on either side of a boundary. Routine changes within the model need only communication; a change that breaks the model warrants prior consultation, with the consumer holding a real option to push back.
2
Keep decisions consistent and recorded
Give an established concept one specification and reuse it. When a decision is revisited, record the new decision and its reason in a place both sides can see, so the same discussion is not repeated and the codebase does not oscillate.
3
Make every change explicit
Announce changes when requesting them, however small they seem, and describe the resulting behavior change in the pull request. Splitting work into small, independently revertible units keeps each change explainable — see Pull Requests and Task Breakdown.
4
Maintain the change-detection net
Keep types derived from their single source of truth and keep tests asserting observable behavior, so the reach of any change — including one requested by another role — surfaces mechanically. See Test Code.
Related pages
Pull Requests
Keep each change small, explainable, and reviewable — the unit in which changes are made explicit.
Task Breakdown
Split work into independently revertible units so every change stays cheap to undo.
Test Code
Build the change-detection net that makes revisions safe.
Release Strategy
Ship changes progressively so even large changes stay reversible.