Cohesion and its Relation to the Single Responsibility Principle SRP
Cohesion is the degree to which the methods and fields of a class all read and write the same state in service of one job — and it matters because the cost of a change is proportional to how many unrelated reasons a class has to be edited: every extra responsibility welded into one class is another set of teammates, deadlines, and test suites that collide on the same file.
The classic UserManager-that-also-does-file-IO is the textbook smell, but a smell only teaches you if you can feel the bite. Below we trace one concrete change request through the low-cohesion version and watch it force edits and a redeploy in code that has nothing to do with the change.
The low-cohesion class — with real bodies, not stubs
Here is the "everything in one bin" version, fleshed out so the coupling is visible. UserManager holds an in-memory user list and owns the on-disk persistence format. Two unrelated reasons to change now live in one file.
public class UserManager {
private final List<String> users = new ArrayList<>();
private final String path = "users.txt";
public void addUser(String user) {
users.add(user);
persist(); // user logic reaches into file logic
}
public void deleteUser(String user) {
users.remove(user);
persist();
}
// --- file responsibility, bolted onto the same class ---
private void persist() {
try (FileWriter w = new FileWriter(path)) {
for (String u : users) w.write(u + "\n"); // newline-delimited
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public List<String> loadUsers() {
try (BufferedReader r = new BufferedReader(new FileReader(path))) {
List<String> out = new ArrayList<>();
String line;
while ((line = r.readLine()) != null) out.add(line);
return out;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}The Go shape is the same disease — one struct owning both the membership rule and the wire format:
type UserManager struct {
users []string
path string
}
func (m *UserManager) AddUser(u string) error {
m.users = append(m.users, u)
return m.persist() // membership logic calls IO logic
}
func (m *UserManager) persist() error {
return os.WriteFile(m.path, []byte(strings.Join(m.users, "\n")), 0o644)
}The change that bites: "persist users as JSON, not newline-delimited text"
Ops asks for one thing: stop writing users.txt as bare lines and start writing JSON so another service can read it. That is a storage-format decision. Watch how it ripples through the low-cohesion class, and then through the split version.
| Step | Low-cohesion UserManager | Split UserManager + UserRepository |
|---|---|---|
| 1. Where do you edit? | Inside UserManager.java — the same file that owns addUser/deleteUser. | Inside UserRepository only. UserManager is untouched. |
| 2. What must you re-test? | All of UserManager's tests — add/delete logic is in the blast radius even though it didn't change. | Only the repository's serialization tests. Membership tests stay green by construction. |
| 3. Who else is editing this file? | The teammate adding email-validation to addUser — now you have a merge conflict on an unrelated change. | None — different concern, different file. |
| 4. Blast radius | 1 storage change forces a redeploy of the class that holds the core business rule. | Storage change is isolated; business rule binary need not even recompile if behind an interface. |
That row 2 is the whole lesson: low cohesion makes the test/redeploy blast radius bigger than the change. You touched the file for a file-format reason and dragged the user-membership rules into the diff with you.
The high-cohesion split — corrected and compiling
Pull the storage concern behind a UserRepository interface. Now UserManager reads as one job (membership), and the JSON change lands entirely inside FileUserRepository. The interface is what makes the "don't even recompile the business rule" claim true.
public interface UserRepository {
void save(List<String> users);
List<String> load();
}
public class UserManager {
private final List<String> users = new ArrayList<>();
private final UserRepository repo;
public UserManager(UserRepository repo) { this.repo = repo; }
public void addUser(String user) {
users.add(user);
repo.save(users); // delegates; does NOT know the format
}
public void deleteUser(String user) {
users.remove(user);
repo.save(users);
}
}
// The storage-format change lives entirely here:
public class FileUserRepository implements UserRepository {
private final String path = "users.json";
public void save(List<String> users) {
// swap newline format -> JSON, touching ONLY this class
String json = users.stream()
.map(u -> "\"" + u + "\"")
.collect(Collectors.joining(",", "[", "]"));
try { Files.writeString(Path.of(path), json); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
public List<String> load() { /* parse JSON */ return new ArrayList<>(); }
}type UserRepository interface {
Save(users []string) error
Load() ([]string, error)
}
type UserManager struct {
users []string
repo UserRepository // depends on the interface, not the file
}
func (m *UserManager) AddUser(u string) error {
m.users = append(m.users, u)
return m.repo.Save(m.users)
}
// JSON change is confined here:
type FileUserRepository struct{ Path string }
func (r FileUserRepository) Save(users []string) error {
b, err := json.Marshal(users)
if err != nil {
return err
}
return os.WriteFile(r.Path, b, 0o644)
}
func (r FileUserRepository) Load() ([]string, error) { return nil, nil }Why the naive split (no interface) is only half-right
The original lesson split into UserManager and FileManager as two concrete classes — better, but if UserManager still does new FileManager() internally, the membership class still names a concrete storage class. You've fixed cohesion but left a hard dependency: swapping file storage for a database still forces an edit and recompile of UserManager. Introducing the UserRepository interface and injecting it (constructor parameter above) is what actually buys you the "business rule doesn't recompile" isolation. Cohesion and decoupling are two separate wins; the interface delivers both.
Pitfalls a working engineer hits
- Over-splitting into anemic classes. Chasing "one method per class" gives you 12 single-method classes that must all be wired together. Cohesion is about relatedness of state and purpose, not method count. A class with five methods that all touch the same fields is more cohesive than five one-method classes that pass the same blob around.
- Coincidental cohesion masquerading as the real thing. A
Utils/Helpers/Commonclass is the lowest form of cohesion — methods grouped only because nobody knew where else to put them. It grows monotonically and becomes a merge-conflict magnet. - Sequential cohesion that hides a leak.
addUsercallingpersistinternally looks tidy but means membership and durability are temporally coupled — you cannot add a user without writing to disk, so unit-testing the rule needs a real file. The interface split breaks that. - Splitting by noun, not by reason-to-change. SRP's "single reason to change" is about actors: if the finance team and the ops team both ask for edits to the same class, it has two reasons to change regardless of how focused the nouns look. Cohesion measured by "who requests changes" beats cohesion measured by topic name.
When to split for cohesion — and when NOT to
Decision signal: split a class when two clusters of its methods touch disjoint sets of fields, or when two different actors/teams keep editing it for unrelated reasons. If you can draw a line through the class and each half shares no state with the other, that line is a class boundary screaming to exist.
Trade-off vs. the alternative (leave it merged):
- Splitting (high cohesion + interface) — gain: smaller blast radius, independent testing, swappable storage, parallel team work. Cost: more classes and files, an interface to navigate (indirection), and a wiring point (constructor/DI container) that newcomers must trace to see what implementation is actually used.
- Keeping it merged — gain: one file, no indirection, trivial to read end-to-end for a tiny app. Cost: every change re-tests and re-deploys the whole class; concurrent edits conflict; storage and rules cannot evolve independently.
Contrast with a Facade, which deliberately groups several subsystems behind one class for caller convenience — that is low internal cohesion accepted on purpose to simplify the API surface. Cohesion is a default to pursue, not an absolute: a Facade trades it away knowingly.
Choose the split when the class has more than one actor requesting changes or two disjoint field-clusters; prefer leaving it merged when the whole thing is <~50 lines, has one owner, and the "second responsibility" is one trivial method you'd otherwise create a class-with-ceremony for.
Traced decision: our 4-method UserManager — two methods touch users, two touch path/file format; two actors (the team owning business rules, the team owning persistence). Two disjoint field-clusters + two actors = split. A 2-method Stack whose push/pop both touch the same array = leave it; there is nothing to separate.
Takeaways
- Cohesion is measured by shared state and shared reason-to-change, not by line count or topic name — high cohesion is what makes a class's change-blast-radius small.
- The bite of low cohesion is concrete: a storage-format change dragged the user-membership rule into the diff, the test suite, and the redeploy. The split confines each change to one box.
- SRP is the design rule ("one reason to change"); high cohesion is the structural property that satisfies it. Add an interface on the seam to also break the dependency, or the split only fixes cohesion, not coupling.
- Don't over-split into anemic classes or accumulate a
Utilsgrab-bag — and accept low cohesion deliberately (Facade) only when caller convenience is worth it.
Sources: Robert C. Martin, Clean Architecture and Agile Software Development: Principles, Patterns, and Practices (the "single reason to change / single actor" framing of SRP); Larry Constantine & Edward Yourdon, Structured Design (the original cohesion spectrum — coincidental through functional); Steve McConnell, Code Complete 2e (cohesion as a routine/class quality). Re-authored and deepened for this guide: replaced empty method stubs with compiling Java and Go, added a concrete "persist as JSON" change scenario with a blast-radius trace, a before/after class-map diagram, the interface-injection correction, and an explicit when-to-split trade-off against leaving the class merged and against the Facade pattern.
🤖 Don't fully get this? Learn it with Claude
Stuck on Cohesion and its Relation to the Single Responsibility Principle SRP? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Cohesion and its Relation to the Single Responsibility Principle SRP** (OO & Low-Level Design) and want to truly understand it. Explain Cohesion and its Relation to the Single Responsibility Principle SRP from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Cohesion and its Relation to the Single Responsibility Principle SRP** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Cohesion and its Relation to the Single Responsibility Principle SRP** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Cohesion and its Relation to the Single Responsibility Principle SRP** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.