On this page
Description
The Single Responsibility Principle says a class should have one, and only one, reason to change. It's the most quoted SOLID principle and the most misread: it isn't about doing "one thing" or having one method, but about not letting unrelated kinds of change collide in the same code. This skill covers what a "reason to change" is, how to find the seams in a class, and how to avoid splitting too far.
Here's the kind of class it's about:
Java
class Invoice {
double calculateTotal() { /* tax and discount rules */ }
String toPdf() { /* fonts, layout, margins */ }
void saveToDatabase() { /* SQL, connection handling */ }
}
Python
class Invoice:
def calculate_total(self) -> Money: ... # tax and discount rules
def to_pdf(self) -> bytes: ... # fonts, layout, margins
def save_to_database(self) -> None: ... # SQL, connection handling
Three different groups care about this class: the finance team (tax rules), the design team (the PDF layout) and whoever owns the database schema. A change requested by any of them edits the same file - and a margin tweak can accidentally break a tax calculation that shares a helper method.
We'll go from simple to more technical:
- The principle, and what "a reason to change" means.
- Splitting a class along its reasons to change, step by step.
- Where you'll see it in real architectures.
- How far to split - and when splitting makes things worse.
The principle
A class should have one, and only one, reason to change.
"Reason to change" means a source of change - usually a person or team with a distinct concern. Split the class so each part changes for one reason:
Java
class InvoiceCalculator { Money total(Invoice inv) { ... } } // finance rules
class InvoicePdfRenderer { byte[] render(Invoice inv) { ... } } // presentation
class InvoiceRepository { void save(Invoice inv) { ... } } // persistence
Python
class InvoiceCalculator:
def total(self, inv: Invoice) -> Money: ... # finance rules
class InvoicePdfRenderer:
def render(self, inv: Invoice) -> bytes: ... # presentation
class InvoiceRepository:
def save(self, inv: Invoice) -> None: ... # persistence
Now a new tax rule touches only InvoiceCalculator, and a layout change can't break it. In Python the unit is often a module rather than a class - pricing.py, pdf.py and storage.py, with plain functions where no state is needed; the principle is the same.
How to apply it, step by step
- List who would ask for changes to this class, and why.
- Group the methods by those reasons - that's where the seams are.
- Move each group into its own class with a name that states its one job.
- Let a small coordinating class (or the caller) wire them together.
- Stop when each class has one clear reason to change - not when every class has one method.
Where you'll see it
- Layered architectures: controllers (HTTP), services (business rules) and repositories (storage) change for different reasons.
- Separating what to compute from how to present or store it - calculators vs. formatters vs. repositories.
- Logging, metrics and security pulled out of business classes into filters, decorators or aspects.
Which tool for which job?
| You notice | Do this |
|---|---|
| Different teams or stakeholders ask for changes to one class | split it by who asks: one class per actor |
| Calculation, presentation and storage in one class | calculator, renderer/formatter, repository |
| A shared private helper used by two different concerns | give each concern its own copy, or an explicit shared rule both own |
| Logging, metrics or security mixed into business methods | decorators, middleware or filters around the business class |
| Several small classes that always change together | merge them back - they're one responsibility |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| One method per class | ten files to follow one feature | split by reasons to change, not method count |
| Splitting by technical layer only | classes that always change together, spread thin | keep what changes together, together |
Names with "And", Manager, Helper, Util | several jobs hidden behind a vague name | name each class after its one job |
| Sharing a helper between two actors to avoid duplication | one team's change silently breaks the other's results | duplicate it - it only looked the same |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. UserService validates input, hashes passwords, stores users and sends welcome emails. How many reasons to change?
Show answer
At least four: validation rules, security/hashing policy, storage, and email content/delivery. Each is a candidate for its own class, with UserService coordinating them.
2. Is "one method per class" what SRP asks for?
Show answer
No - SRP is about one reason to change. A class can have many methods that all serve the same concern.
3. What's a quick red flag for an SRP violation in a class name?
Show answer
The word "And" (or vague names like Manager, Helper, Util) that hide several jobs.
Key takeaways
- One class, one reason to change - one actor it answers to.
- Find seams by asking who requests changes and why.
- Split calculation, presentation and persistence; coordinate them from outside.
- Don't over-split: things that always change together belong together.