> ## Documentation Index
> Fetch the complete documentation index at: https://lib.findy.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# Modifiability

> Classify software changes into three kinds — separate, revise, and remove — and learn the patterns that break modifiability and how to prevent them.

## 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.

| Kind     | What stays the same                                | What changes                             | Typical examples                                                      |
| -------- | -------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------- |
| Separate | The inputs and outputs of each unit                | The arrangement, presentation, or volume | Moving content from a modal to a page; adding pagination              |
| Revise   | The conceptual model behind the units              | The representation of inputs or outputs  | Adding an image to a card; adding an input field; changing sort order |
| Remove   | The behavior of everything around the removed unit | The unit itself disappears               | Retiring a feature; deleting an obsolete screen                       |

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.

```tsx Running example: an order history list theme={null}
// Everything below lives in the @order-history module.

// One order. This unit owns the representation.
type OrderItemProps = { order: Order };

export const OrderItem = ({ order }: OrderItemProps) => (
  <li>
    <span>{order.title}</span>
  </li>
);

// The list of orders. Data comes in from outside.
type OrderListProps = { orders: Order[] };

export const OrderList = ({ orders }: OrderListProps) => (
  <ul>
    {orders.map((order) => (
      <OrderItem key={order.id} order={order} />
    ))}
  </ul>
);

// OrderList used inside a modal
export const OrderHistoryModal = ({ orders }: { orders: Order[] }) => (
  <Modal title="Order history">
    <OrderList orders={orders} />
  </Modal>
);
```

### 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.

```tsx Separate: from a modal to a page theme={null}
// The container changes from a modal to a page.
// OrderList moves as-is — not a single line inside it changes.
export const OrderHistoryPage = ({ orders }: { orders: Order[] }) => (
  <PageLayout title="Order history">
    <OrderList orders={orders} />
  </PageLayout>
);
```

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.

```tsx Revise: add a product image theme={null}
// Only OrderItem — the unit that owns the representation — responds.
export const OrderItem = ({ order }: OrderItemProps) => (
  <li>
    <img src={order.thumbnailUrl} alt="" /> {/* added */}
    <span>{order.title}</span>
  </li>
);
// OrderList and OrderHistoryPage stay untouched.
// Order gains thumbnailUrl, so types and tests follow the change.
```

What makes revisions safe is **change detection** — the ability to see, cheaply and mechanically, where the effects of a change reach:

<CardGroup cols={2}>
  <Card title="Static types" icon="shield-check">
    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.
  </Card>

  <Card title="Automated tests" icon="vial">
    Tests that assert observable behavior fail exactly where a revision alters behavior, turning "what did this break?" into a mechanical question. See [Test Code](/development/testing).
  </Card>
</CardGroup>

<Info>
  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.
</Info>

### 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.

```diff Remove: retire the order history feature theme={null}
  // routes.tsx — remove the usage and the import.
  // With its entry point no longer imported, the @order-history
  // module — OrderList and OrderItem included — can be deleted whole.
- import { OrderHistoryPage } from "@order-history";

  export const routes = (
    <Routes>
      <Route path="/" element={<HomePage />} />
-     <Route path="/orders/history" element={<OrderHistoryPage />} />
    </Routes>
  );
```

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:

```tsx Treating a separation as a revision theme={null}
// Two ways to handle "move the modal content to a page"

// ❌ Treated as a revision: the list rendering is rewritten for the page
// (the same view now exists twice, and a later revision such as
// adding a product image must be paid in both places)
export const OrderHistoryPage = ({ orders }: { orders: Order[] }) => (
  <PageLayout title="Order history">
    <ul>
      {orders.map((order) => (
        <li key={order.id}>
          <span>{order.title}</span>
        </li>
      ))}
    </ul>
  </PageLayout>
);

// ✅ Treated as a separation: OrderList moves to the new container as-is
export const OrderHistoryPage = ({ orders }: { orders: Order[] }) => (
  <PageLayout title="Order history">
    <OrderList orders={orders} />
  </PageLayout>
);
```

**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:

```typescript Re-declared vs. derived types theme={null}
// greeting.ts
type GreetingArgs = { name: string };
export const greeting = ({ name }: GreetingArgs) =>
  `Nice to meet you, ${name}!`;

// message.ts

// ❌ Re-declares the same shape by hand
// (when greeting's definition changes, this type silently keeps the old shape)
type MessageArgs = { name: string } & { agent: Agent };

// ✅ Derives the input type from greeting
// (a change to greeting's definition propagates to this type
// and its callers as type errors)
type MessageArgs = Parameters<typeof greeting>[0] & { agent: Agent };

export const message = ({ name, agent }: MessageArgs) =>
  `Hello, I am ${agent.name}. ${greeting({ name })}`;
```

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.

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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](/development/pull-request) and [Task Breakdown](/development/task-breakdown).
  </Step>

  <Step title="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](/development/testing).
  </Step>
</Steps>

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.

## Related pages

<CardGroup cols={2}>
  <Card title="Pull Requests" icon="code-pull-request" href="/development/pull-request">
    Keep each change small, explainable, and reviewable — the unit in which changes are made explicit.
  </Card>

  <Card title="Task Breakdown" icon="list-check" href="/development/task-breakdown">
    Split work into independently revertible units so every change stays cheap to undo.
  </Card>

  <Card title="Test Code" icon="vial" href="/development/testing">
    Build the change-detection net that makes revisions safe.
  </Card>

  <Card title="Release Strategy" icon="rocket" href="/development/release-strategy">
    Ship changes progressively so even large changes stay reversible.
  </Card>
</CardGroup>
