Case file A4

Common Closure Principle

Things that change together belong together.

Code that changes for the same reason belongs in the same component. Code that changes for different reasons belongs in different components. Group by axis of change, not by category of object.

ByAdam LewisPublished3 May 2026Revised13 Oct 2026Reading12 minVersionv1.2ConfidenceHigh
§0b

Opinion

I've had this argument with senior architects more times than I can count, usually when a folder labelled shared/ shows up in twenty-three pull requests in a single sprint. The folder is not shared. It is a dumping ground where two unrelated reasons-to-change have been hiding under one name. Robert C. Martin spelled this out in 2002 and again in 2017: classes that change together belong together, classes that change for different reasons belong apart.1 Most teams miss the corollary: a component that does not change for the reasons its neighbours change should not live in the same component as them.

The link to the Open-Closed Principle is the word “closure,” and Martin chose it on purpose. OCP's “closure” is Bertrand Meyer's (closed against modification, open to extension): a class-level rule about how to add features without editing the base.2 Martin says CCP deals with closure “in the OCP sense of the word”. Full closure is not attainable, so CCP groups into one package the classes that cannot be closed against the same kinds of change, and a change in requirements then has a good chance of touching a minimal number of packages. Same word, same idea, one scale up.6

The lever that makes CCP visible is the same one that makes A1 Screaming Architecture visible: the folder tree. If every PR touches three folders, the folders are wrong. If most PRs touch one folder, the folders are matching the axes of change, which is what CCP is asking for. Co-locate by reason-to-change and the diff stops looking like a tangle.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

Things that change together belong together. A component should have one reason to change, when a single folder appears in PRs for two unrelated requirements, split it. Group by axis of change (orders, billing, identity), not by category (controllers, services, models).

/tenet/common-closure-principle/A4
Copy for paste
§0c

AI eyes only

Rule: things that change together belong together. Group by reason-to-change, not by type.

Reject: top-level controllers/, services/, models/ that scatter one feature across three folders. Reject: extracting by category when the requirement is a single closure.

Generate: per-feature folders that contain the controller, service, model and tests for that one feature. Refactor by axis-of-change before refactoring by type.

Diagnostic: when a single requirement changes, the diff confines to one folder. If the diff spans three category folders, the architecture has split a single closure across them.

§0d

Why?

  • Feature PRs touch one folder. Code review reads one folder. The diff stops looking like a hairball spread across four category-shaped directories and the merge conflict surface shrinks with it.
  • Caps the blast radius of a release. When a component has one reason to change, a deploy of that component changes one capability: the rest of the system isn't even rebuilt. Continuous deployment becomes a smaller risk decision.
  • Pairs cleanly with F1 Single Responsibility: SRP at the class, CCP at the package. The same instinct at two scales, mutually reinforcing. A package full of single-purpose functions naturally has one reason to change.
  • Surfaces axes of change explicitly. The team learns which capabilities move together and which move independently, which is exactly what you need to plan a release train, a rollout window, or a service split.
  • Falls out of A1 Screaming Architecture for free. When folders name domains, the domain's axis of change is the folder's axis of change by construction. The two rules are one rule wearing different hats.
  • Surfaces the split signal early. When a single component starts appearing in PRs for two unrelated requirements, CCP says split it. The rule turns a vague “this folder is getting big” intuition into a concrete refactor cue.
  • Coding agents reach for category-shaped folders because the training data is loud with MVC tutorials. CCP makes the agent answer one question first, what reason-to-change does this code belong to?, and gives it a place to put the answer.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

Robert C. Martin coined the Common Closure Principle in the 1996 C++ Report article “Granularity” and codified it in Agile Software Development: Principles, Patterns, and Practices (2002).6 The single load-bearing line: classes that change together are packaged together. Martin paired CCP with REP (Release-Reuse Equivalency) and CRP (Common Reuse Principle) as the three Component Cohesion principles: each one rules out a different way of getting package boundaries wrong.

The book-length restatement is Chapter 13 of Clean Architecture (Pearson, 2017).1 Martin makes the SRP-at-scale claim explicit: CCP is the SRP for components. A component should have only one reason to change. When it has two, it should split. The chapter pairs CCP with REP (a component is the unit of release) and CRP (don't force consumers to depend on things they don't use) and the three together form the package-cohesion triad.

Matthias Noback's Principles of Package Design (Apress, 2018) is the practitioner companion.7 Chapter 8 walks the CCP rule into worked examples. The chapter's teaching idea, “reasons to change” as the package-design unit, is the sharpest version of the rule in print. The book also pairs CCP with the SDP/SAP rules from A3 Stable Dependencies: the same component principles run together as a single package-design system.

The instinct is older than the name. Larry Constantine's cohesion taxonomy (1968, published 1974) had functional cohesion at the top of the hierarchy: a module exhibits functional cohesion when its parts contribute to a single, well-defined task, which is what “changes for one reason” is when applied to the function rather than the package.8 CCP is the package-level form of the same instinct, given a metric and a name twenty-eight years later.

§2

Quotes

Gather into components those classes that change for the same reasons and at the same times. Separate into different components those classes that change at different times and for different reasons.

Robert C. Martin · Clean Architecture (2017), Ch. 13

The classes in a package should be closed together against the same kinds of changes. A change that affects a package affects all the classes in that package.

Robert C. Martin · Granularity, C++ Report (1996)

Duplication is far cheaper than the wrong abstraction.

Sandi Metz · The Wrong Abstraction (2016)

Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate.

Eric Evans · Domain-Driven Design Reference (2015), Aggregates
§3

Evidence

16 external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Robert C. Martin · 2017
    The single canonical chapter. Pairs CCP with REP and CRP into a triad: a component should have one reason to change, one unit of release, and no classes its consumers don't need.
  2. 02
    Robert C. Martin · 2002
    Source of CCP, REP, CRP and the statement that CCP is the SRP for components. The 2002 book is the bridge between the 1996 C++ Report papers and the 2017 Clean Architecture restatement.
  3. 03
    Robert C. Martin · 2002 onwards
    Online compendium of all SOLID + package-design principles in one page. CCP listed alongside REP and CRP as the package-cohesion triad. Useful for the family-tree idea.
  4. 04
    Matthias Noback · 2018
    Apress book. Ch. 8 walks the CCP rule into worked examples. The “reasons to change” lesson is the sharpest version of the rule in print. The single best practitioner companion.
  5. 05
    DevLead.io · 2022
    Short tutorial that walks through CCP with a small Switch/Lamp component diagram. Useful as the one-paragraph entry point for readers who want the rule before they read the book chapter.
  1. 06
    Robert C. Martin · 1996
    The fifth of Martin's Engineering Notebook columns for the C++ Report, stating CCP beside REP, CRP and ADP: “The classes in a package should be closed together against the same kinds of changes.” Martin ties the rule to the Open-Closed Principle himself: the closure is “in the OCP sense of the word”.
  2. 07
    Vlad Khononov · 2021
    Modern restatement that grounds package boundaries in DDD's subdomain types (core, supporting, generic). Qualifies CCP by saying the axis-of-change is usually the subdomain boundary, not just the change history.
  3. 08
    Vaughn Vernon · 2013
    Vernon's aggregate-shape rules. Holds the harder reading: package boundaries should be drawn by aggregate (transactional consistency boundary), and CCP's grouping has to bend around the model when the two diverge.
  4. 09
    Sandi Metz · 2016
    “Duplication is far cheaper than the wrong abstraction.” Applied to packages: a folder grouping built on yesterday's axis-of-change is one that next quarter's requirement teaches you was wrong. Carved out via the wait-for-three-changes trigger.
  5. 10
    Eric Evans · 2003
    The aggregate-shape counter. Evans groups by transactional boundary; CCP groups by reason-to-change. In a healthy domain they converge. When they diverge, the model boundary wins and CCP bends around it.
  6. 11
    Alistair Cockburn · 2005
    The architectural-rings counter. Cockburn draws the line that matters between the inside of the application and the outside world, joined by ports and adapters, and code on the inside must not leak into the outside. The article says nothing about packages or change history; it adds an inside-outside axis that CCP's axis-of-change rule does not cover.
  7. 12
    Jimmy Bogard · 2018
    VSA produces the same kind of folder shape CCP predicts: features that change together live in one folder. Useful complement: VSA covers the positive half of CCP (group what changes together); CCP also covers the negative half (split what doesn't).
  8. 13
    Randy Coulman · 2014
    Two-part walkthrough of Martin's component principles. Part 1 covers REP, CCP, and CRP: the cohesion triad. Useful for showing how CCP composes with its siblings rather than standing alone.
  9. 14
    WikiWikiWeb contributors · 2003 onwards
    The community wiki entry. The short discussion suggests the rule should read the other way round, and asks whether it is just cohesion. It never mentions OCP.
  10. 15
    John O'Hanley · javapractices.com · 2013
    Java-community restatement of the same idea: package by feature (axis-of-change), not by layer (category). Predates CCP terminology in this audience but lands the same rule.
  11. 16
    Simon Brown · 2015
    Modernised version of the package-by-feature rule with the C4-model vocabulary. Useful as the bridge between CCP's axis-of-change rule and Brown's architectural-component vocabulary.

Sixteen sources, two stances. The supporters cluster on Martin's package-cohesion canon and the SRP-at-scale idea, with Noback's Principles of Package Design grounding the day-to-day. The qualifiers further down hold the harder reading: axes-of-change are easier to name in retrospect than in prospect, and forcing co-location too early can fossilise a classification that the next requirement reveals to be wrong. The steelman is theirs too: the DDD camp would draw package boundaries by aggregate, not by what changes together this quarter. Cockburn adds an axis CCP does not cover: the line between the application and the outside world.

§4

Examples

Viewing: TypeScript.
Avoid

Before payment files scattered across three folders. One rule change touches three.

File{routes,types,services}/...
types/payment-types.ts
export type Donation = { amountPence: number; sanctuary: string };
services/payment-processor.ts
import { Donation } from "@/types/payment-types";
export const applyGiftAid = (d: Donation) => d.amountPence * 1.05;
routes/payment/route.tsthird folder, same change axis
import { applyGiftAid } from "@/services/payment-processor";
Prefer

After one payment/ folder. One reason to change, one folder to edit.

Filepayment/...
payment/payment.types.ts
export type Donation = { amountPence: number; sanctuary: string };
payment/apply-gift-aid.tssame folder
import { Donation } from "./payment.types";
export function applyGiftAid(donation: Donation): number {
  return donation.amountPence * 1.05;
}
payment/route.tssame folder again
import { applyGiftAid } from "./apply-gift-aid";
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
import/no-cycleeslint-plugin-importimport cycles between sibling components: the symptom that two components have grown into each other rather than past each other; almost always a missing extraction.
import/no-internal-moduleseslint-plugin-importdeep imports into a component's internals when only the public surface was meant to be reached: the import shape that defeats CCP's axis-of-change boundary.
import/max-dependencieseslint-plugin-importfiles that import from too many places: a leading indicator that the component owning that file has grown a second reason to change.
jscpdjscpdduplicate code blocks across components, reported from the second copy: the same code in two components means one change must touch both, which is the CCP signal to give it a single home.
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';

export default defineConfig({
  files: ['**/*.{ts,tsx}'],
  languageOptions: { parser: tseslint.parser },
  settings: {
    'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] },
    'import/resolver': { node: { extensions: ['.ts', '.tsx', '.js'] } },
  },
  plugins: { import: importPlugin },
  rules: {
    'import/no-cycle': ['error', { maxDepth: 10 }],
    'import/no-internal-modules': ['error', {
      allow: ['**/index.ts', '**/*/public/**'],
    }],
    'import/max-dependencies': ['error', { max: 12 }],
  }
});
§4c

AI rules

File.cursor/rules/a4-common-closure.mdc
---
alwaysApply: true
---

## Prickles A4: Common Closure Principle

Group code by axis of change. What changes for the same reason goes in the same component, and what changes for different reasons goes in different components.

A component should have one reason to change. When a single component starts appearing in PRs for two unrelated requirements, split it.

CCP is the SRP for components. Don't group by category (controllers, services, models). Group by what changes together (orders, billing, identity).

Wait for the third change-together signal before forcing the group. The wrong package boundary is more expensive than the duplication it would have eliminated.

Repo layout, CI, and ESLint wiring for these paths live on /implementation, not repeated on every tenet.

§5

Counter-argument

Counter

The strongest steelman is Sandi Metz combined with the DDD aggregate camp.3 “Things that change together” is a property the team can only see in retrospect. In prospect it is a guess and a folder structure built on a guess is one the next requirement reveals to be wrong. The Wrong Abstraction tax applies to packages too: a component that grouped two features because they changed together in Q1 holds the cost of that grouping when Q2 says one of the features needs to be released separately. The DDD camp sharpens the same point: the right grouping is the aggregate, defined by transactional boundary, not by historical axis of change. CCP's rule, applied without that discipline, can produce components that respect change history but violate the model.

§6

Counter-argument retort

Reply

Metz is right that yesterday's grouping is a guess about tomorrow's requirements. The carve-out is the same one Three-Tier Hoisting uses: don't group until you have seen the duplication.3 CCP doesn't fire on the first feature. It fires when two features that have always shipped together stop being two features and start being one component's worth of work. Until then, leave them in their own folders. Once the change history makes the grouping obvious, the rule says collect them. The Metz tax exists. The rule respects it by waiting for the third change-together signal before forcing the group.

The DDD aggregate counter is the more interesting one because it is a different decomposition rather than a wrong one.4 Evans groups by transactional boundary, Vernon by consistency boundary, Khononov by subdomain category. CCP groups by reason-to-change. In a healthy domain those three groupings converge: an aggregate that protects an invariant changes when that invariant's rules change, which is the same axis-of-change CCP names. When they diverge, the team has a real architectural decision to make: the model boundary is the one to defend, and CCP's grouping has to bend around it.5 CCP isn't a replacement for aggregate design, but the rule for the part of the package boundary aggregate design hasn't already settled.

The OCP link is the genuine residue and the case file pins it: the shared word is Martin's choice, not an accident.2 OCP is class-level, about extension without modification (Meyer's “closure”). CCP takes the same closure to the package: it groups the classes that cannot be closed against the same kinds of change, so a change in requirements has a good chance of touching a minimal number of packages. Same idea, one altitude up, and the altitude is what makes them separate rules, just as it separates F1 (function-scale) from A4 (package-scale). See F1 Single Responsibility for the function-scale form. This is the package-scale form.

The novelty challenge, that “package by feature” or “vertical slices” already cover this, is partially fair. The difference CCP adds is the negative half of the rule. Vertical slices say “put feature things together”. CCP also says “don't put together things that change for different reasons”, the rule that carves features apart when they grow into separate change axes. Vertical Slice Architecture handles the positive half; CCP handles both halves.

§7

Notes

  1. [1]Robert C. Martin: Clean Architecture (Pearson, 2017), Ch. 13 “Component Cohesion”. Pairs CCP with REP and CRP: a component should have one reason to change, one unit of release, and no classes its consumers don't need.
  2. [2]Bertrand Meyer / Robert C. Martin: Open-Closed Principle: Meyer, Object-Oriented Software Construction (1988) and Martin, “The Open/Closed Principle” (2014). The two “closures” are the same idea at two scales: Martin says CCP deals with closure in the OCP sense, grouping into one package the classes that are closed against the same kinds of change.
  3. [3]Sandi Metz: “The Wrong Abstraction” (2016): “Duplication is far cheaper than the wrong abstraction.” Applied to packages: a grouping built on yesterday's axis-of-change is one that next quarter's requirement teaches you was wrong.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-10-13