Common Closure Principle: Group Code That Changes Together

A codebase can have tidy classes and still make routine changes expensive. A new pricing rule might require edits in a controller module, a generic utilities package, a shared validation package, and a reporting package. Each edit is small, but one business decision now crosses several boundaries.

The Common Closure Principle offers a practical way to judge those boundaries: code that tends to change for the same reason should tend to live in the same component. A component can be a package, module, library, or another unit that a team changes and releases together.

The goal is not to predict every future edit. It is to reduce change dispersion: the number of components that must move together when one coherent requirement changes.

The mental model: optimize boundaries for change

Imagine an order system with these modules:

controllers/
    checkout

validators/
    discount

calculators/
    discount

formatters/
    discount-summary

The directory structure groups code by technical role. That can look consistent, but suppose discount policy changes frequently. A single policy update may require coordinated edits across validators, calculators, and formatters.

A different boundary might group the policy-related code:

discounts/
    eligibility
    calculation
    summary

Now a discount-policy change is more likely to remain inside one component.

The Common Closure Principle does not say that feature folders are universally preferable to technical layers. It says that change patterns are evidence about cohesion. If several pieces repeatedly change as part of the same decision, their current separation may be creating coordination cost.

Closure means containing a class of changes

The word closure here is about containing the impact of change. A component is well closed against a kind of change when that change can usually be implemented inside the component without forcing edits elsewhere.

Suppose tax calculation depends on:

  • jurisdiction rules,
  • exemption rules,
  • rounding policy,
  • the tax lines shown on an invoice.

If those details all change when tax policy changes, placing them near each other can create a useful boundary. The rest of the system can depend on a smaller contract such as:

TaxResult calculateTax(Order order, Address destination)

A policy change may alter several internal functions while leaving that contract stable.

This is not absolute isolation. A significant requirement can legitimately cross components. The principle is useful when repeated changes reveal a stable pattern: the same set of files or modules keeps moving together.

Change reasons matter more than surface similarity

Two pieces of code can look similar yet belong to different change boundaries.

Consider date formatting in two contexts:

formatInvoiceDate(date)
formatAuditTimestamp(time)

Both format temporal values, so a generic date-utils package may seem natural. But their reasons for change can differ. Invoice formatting may follow customer-facing document requirements. Audit timestamps may follow an operational logging contract.

Combining them because both manipulate dates creates structural similarity without change cohesion. A later invoice-format change could then modify a shared package that also serves auditing.

The reverse also occurs. Code with different technical shapes can belong together because one business decision governs all of it. A validator, calculator, and mapper may form one cohesive component if the same policy change routinely affects all three.

This gives a useful design question: which decision would cause this code to change? Components become easier to reason about when their contents have closely related answers.

Use repository history as evidence

Teams do not need to guess all change patterns from diagrams. Version history can provide concrete signals.

If the same files repeatedly appear in the same commits or pull requests, inspect the reason. Frequent co-change can indicate that the files belong to one change boundary.

For example, imagine six recent pricing changes. Each one touches:

pricing/rules
checkout/price-validator
invoices/price-breakdown

That pattern does not prove the modules should be merged. The files might have legitimate independent responsibilities, and large mechanical changes can create misleading co-change. Still, repeated coordinated edits are worth investigating.

Ask what connects them:

pricing policy
    |
    +--> eligibility rule
    +--> amount calculation
    +--> invoice representation

If one policy decision controls all three, a pricing component with an explicit outward contract may reduce future dispersion.

History is especially useful because component boundaries are hypotheses. The codebase’s actual change record can confirm or challenge those hypotheses over time.

A small refactoring example

Suppose an application places subscription cancellation logic in three components:

validation/
    cancellation-window

billing/
    cancellation-refund

notifications/
    cancellation-message

A policy change now says that annual plans can be cancelled within 30 days, with a prorated refund and a specific confirmation message. One requirement touches all three components.

If cancellation policy is a cohesive business capability, the code can be reorganized behind one boundary:

cancellation/
    eligibility
    refund-policy
    confirmation

Other parts of the application call a narrow entry point:

CancellationResult cancelSubscription(
    Subscription subscription,
    CancellationRequest request
)

The exact API depends on the system. The design point is that eligibility, refund policy, and confirmation data can evolve together without requiring unrelated components to understand the policy.

This refactoring does not require putting database access, email delivery, payment gateways, and every cancellation-related operation into one giant module. External mechanisms can remain behind their own boundaries. The cancellation component can decide what should happen and collaborate through explicit interfaces.

Cohesion is about shared reasons for change, not collecting every operation that appears in one workflow.

Do not turn the principle into feature-folder dogma

Grouping by change can improve locality, but taken too far it creates duplication or oversized components.

A stable capability genuinely used by many areas may deserve its own component. Currency arithmetic is one example. If several domains use the same well-defined money operations and those operations change according to their own rules, copying them into each feature would weaken the design.

The principle also competes with other design forces. A component boundary may need to reflect:

  • independent deployment,
  • security or trust boundaries,
  • ownership by separate teams,
  • performance constraints,
  • platform dependencies,
  • a stable public API.

These forces can justify separation even when code sometimes changes together.

The useful approach is to treat common closure as one design pressure among several. It helps answer a specific question: does this boundary contain the impact of the changes the system commonly receives?

Watch for false co-change signals

Not every pair of files changed in one commit belongs in the same component.

A broad rename can touch hundreds of files without revealing domain cohesion. Dependency upgrades, formatting passes, generated code, and repository-wide configuration changes can produce the same noise.

Large feature branches can also blur the signal because one commit may contain several unrelated decisions.

Look for repeated semantic co-change instead. The files should move together because the same requirement or policy affects them, not merely because tooling happened to edit them at once.

Code review history can help here. Pull-request descriptions and commit messages often provide the intent that a raw file list cannot show.

Balance common closure against reuse

There is a useful tension between grouping code that changes together and grouping code that is reused together.

Suppose two features share a helper. Moving that helper into a common package removes duplication, but it also means either feature can now cause the shared package to change. If the helper’s behavior is tightly tied to one feature’s policy, the apparent reuse may create coupling.

Before extracting shared code, ask whether the abstraction has its own stable reason for change. If it does, a shared component can be coherent. If it merely happens to contain similar code today, keeping two small implementations can sometimes preserve cleaner change boundaries.

Duplication has a visible cost. Coupling through a weak shared abstraction has a less visible cost: unrelated features become coordinated through a component neither truly owns.

The right choice depends on expected change, not on a rule that duplication must always be removed.

Component size is a consequence, not a target

The Common Closure Principle does not prescribe a package size or file count. A ten-file component can be incoherent, while a larger component can be cohesive if its contents genuinely move under the same set of decisions.

Still, very large components deserve scrutiny. As responsibilities accumulate, the number of independent reasons for change usually grows. A module called common, core, or utils is especially worth inspecting because broad names can hide unrelated responsibilities.

At the other extreme, splitting every class into its own package can make one change cross many boundaries. Fine-grained structure is not automatically modular structure.

A useful component is large enough to contain a coherent class of changes and small enough to have a comprehensible responsibility.

Apply the principle during real changes

You do not need a repository-wide redesign to use common closure. Apply it while making ordinary changes.

When a task touches several components, note the pattern. Ask whether the edits represent one decision or several independent decisions. If one decision repeatedly crosses the same boundaries, consider moving the related behavior behind a more cohesive component.

Keep the refactoring proportional to the evidence. One unusual change is weak evidence. A recurring pattern across several changes is stronger.

After moving code, check the dependency direction as well. A new cohesive component should expose a deliberate contract rather than forcing callers to reach into its internals. Otherwise the files may be physically closer while the system remains logically scattered.

Use change locality as a design signal

The Common Closure Principle gives component design a concrete test: observe how the software changes.

When one coherent requirement repeatedly edits several components, investigate the boundary. When a component changes for many unrelated reasons, inspect its cohesion. When code looks similar but evolves under different decisions, resist sharing it automatically.

The next time a pull request spans several modules, classify the edits by reason. If one reason accounts for most of them, sketch a boundary that could contain that change. Even when you decide not to move anything, the exercise reveals where the current architecture concentrates or disperses maintenance work.