CMD Guide
HomeOO & Low-Level DesignSOLID Principles

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.

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

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

StepLow-cohesion UserManagerSplit 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 radius1 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.

diagram
diagram

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.

java
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<>(); }
}
go
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

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

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


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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes