Skip to main content

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
Volume changes belong here as well. When a growing dataset requires pagination, the shape of each item does not change; only the quantity handled at a time does. A well-separated structure keeps quantity concerns isolated, so a change in data volume touches nothing except the code that manages quantity. Separations become cheap when units are divided along the conceptual data model — the essential entities of the domain — rather than along incidental boundaries such as the shape of an API response or the current layout. A unit built around a stable concept keeps receiving the same input no matter how its surroundings are rearranged.

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
What makes revisions safe is change detection — the ability to see, cheaply and mechanically, where the effects of a change reach:

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
Designing for removal means keeping dependencies explicit and one-directional, so that the question “what depends on this?” has a mechanical answer. Code whose dependents cannot be enumerated cannot be deleted with confidence, and code that cannot be deleted permanently raises the cost of every future change.

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
The classification is a design tool. When writing new code, the question “which future changes should be separations, which revisions, and which removals?” produces concrete decisions: where to place unit boundaries, which types to derive rather than re-declare, which dependencies to keep explicit. How types are declared illustrates the difference preparation makes. The two declarations below implement the same behavior, but respond very differently when the definition they depend on changes:
Re-declared vs. derived types
Suppose 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.
A useful default posture at role boundaries: routine changes are welcome as long as they are communicated; only changes that destroy an agreed concept require negotiation before they happen. This keeps the cost of ordinary collaboration low while protecting the foundation everything else depends on.

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.