Start Learning
Javaneer
Back to stage
Stage 0·SOLID in Practice

Single Responsibility

A class should have one reason to change. Cohesion, the many jobs a 'god class' secretly does, and how splitting by responsibility makes code safe to touch.

14 min readIntermediate
On this page

The Single Responsibility Principle is the most quoted and most misunderstood of the five. It does not mean "a class should do one thing." It means: a class should have one reason to change - one stakeholder, one axis of change, one job. Get this right and most of the other principles follow; get it wrong and you build the LedgerManager god class.

"One reason to change"

Uncle Bob's precise phrasing is that a module should be responsible to one actor. Ask of any class: who would request a change to this, and why? If the answers point at different people or concerns, the class has too many responsibilities.

Look at ledger-legacy's LedgerManager:

class LedgerManager {
    Transaction parseCsvLine(String line) { ... }      // changes if the CSV format changes
    BigDecimal calculateFee(Transaction t) { ... }     // changes if fee rules change
    void saveToDatabase(Transaction t) { ... }         // changes if the DB/schema changes
    String formatReport(List<Transaction> ts) { ... }  // changes if report layout changes
}

Four methods, four different reasons to change, four different stakeholders: the data team owns the CSV format, finance owns fee rules, the DBA owns persistence, and the business owns report layout. A change from any of them forces you to open this one class - and risks breaking the other three, which is exactly the bug from the last lesson.

Cohesion: the real target

SRP is really about cohesion - how strongly a class's parts belong together. The LedgerManager has low cohesion: parsing and report-formatting have nothing to do with each other; they just ended up roommates. Split by responsibility and each piece becomes highly cohesive:

class CsvTransactionParser { Transaction parse(String line) { ... } }
class FeeCalculator        { BigDecimal calculate(Transaction t) { ... } }
class TransactionRepository{ void save(Transaction t) { ... } }
class ReportFormatter      { String format(List<Transaction> ts) { ... } }

Now a CSV format change touches only CsvTransactionParser. A fee change can't possibly break report formatting - they're different classes. The blast radius of every change shrank to one class.

Don't shatter into a thousand pieces

SRP taken too literally produces a swarm of one-method classes that are just as hard to follow as a god class - you trade a tangle for a diaspora. 'One reason to change' is coarser than 'one method.' A FeeCalculator with several related fee methods is one responsibility; splitting it into CalculateBaseFee, CalculateLateFee, and CalculateDiscount classes is over-decomposition. Group by who-asks-for-the-change, not by counting methods.

Responsibilities can hide

Some responsibilities are sneaky. A class that "just processes an order" might also be logging, sending email, and formatting currency - cross-cutting jobs hiding among the domain logic. A tell is the word "and": if describing what a class does needs an "and" ("it validates the transaction and writes the audit log"), you've likely found two responsibilities. Another tell is mixed levels of abstraction in one method - high-level policy interleaved with low-level string formatting.

A Swiss Army knife vs. a chef's kitchen

A god class is a Swiss Army knife where the blade, scissors, and corkscrew are welded into one immovable lump - sharpen the blade and you risk snapping the scissors. SRP is a proper kitchen where the knife, the whisk, and the peeler are separate tools: each is shaped perfectly for its job, you replace the peeler without touching the knife, and a new cook instantly knows which to grab. The point isn't more tools for their own sake - it's that each tool changes independently.

Find the responsibilities

A teammate defends a UserService class with these methods: register(user), hashPassword(pw), sendWelcomeEmail(user), renderProfileHtml(user), saveToDb(user). They say 'it's all about users, so it's one responsibility.' Identify the distinct reasons-to-change hiding here and who owns each.

What does the Single Responsibility Principle actually mean?

Key takeaways

  • SRP means one reason to change - a class should answer to a single actor or concern, not literally 'do one thing'.
  • It's really about cohesion: parts that change for the same reason belong together; parts that don't should be separate classes.
  • Splitting a god class by responsibility shrinks the blast radius - a change to one concern can't break an unrelated one.
  • Don't over-decompose into one-method classes; group by who-requests-the-change, not by counting methods.
  • Hidden responsibilities announce themselves with the word 'and' and with mixed levels of abstraction in one method.
Was this lesson helpful?
Edit this page on GitHub