Module 02 · SOLID and friends · Lesson 1 of 7
Single responsibility
A class should have one reason to change, meaning it answers to one kind of requirement or one group of people. How to spot a class serving several masters, how to split it, and how to avoid splitting so far that the design turns to dust.
Why it matters
When one class calculates invoices, formats them as PDF and saves them to the database, a change requested by finance (a new tax rule) can break what operations depends on (the export format), and every change risks the other two jobs. Merge conflicts cluster in that file, tests need a database to check arithmetic, and nobody can reuse the calculation without dragging the rest along. The single responsibility principle is the first line of defence against classes that grow until they can no longer be changed safely.
The idea
The usual phrasing is "a class should have only one reason to change". The more useful reading: a class should answer to one actor, one source of requirement changes. Tax rules come from finance, layout from the people who read invoices, storage from engineering; code that serves all three will be changed for three unrelated reasons.
Spotting a violation
| Symptom | What it suggests |
|---|---|
| The class description needs 'and' several times | Several responsibilities |
| Groups of methods use disjoint groups of fields | Separate classes hiding in one |
| Imports span unrelated concerns: SQL, HTTP, PDF, email | Business logic tangled with infrastructure |
| Unrelated feature requests keep touching the same file | Several actors share one class |
| Testing one behaviour requires setting up another (a database to test arithmetic) | Responsibilities that should be separable are not |
Splitting well
Separate the policy (business rules) from the mechanisms (storage, transport, formatting). Keep each piece behind a small interface so the rules can be tested with plain objects and the mechanisms can change independently. Then let a thin coordinator, often a service, sequence the pieces for a use case.
final class InvoiceCalculator { // pure rules, no I/O
Invoice calculate(Order order, TaxPolicy tax) { ... }
}
interface InvoiceRenderer { byte[] render(Invoice invoice); } // PDF today, HTML tomorrow
interface InvoiceRepository { void save(Invoice invoice); }
final class BillingService {
private final InvoiceCalculator calculator; private final InvoiceRenderer renderer;
private final InvoiceRepository repository; private final TaxPolicy tax;
// constructor omitted
byte[] bill(Order order) {
Invoice invoice = calculator.calculate(order, tax);
repository.save(invoice);
return renderer.render(invoice);
}
}
Do not shatter the design
Taken too far, the principle produces a swarm of one-method classes whose behaviour can only be understood by reading ten files, which is worse than the original. A responsibility is a cohesive set of things that change together, not a single method. If two pieces always change together for the same reason, they belong together. Split when you can name two different actors or reasons, or when a test is painful because concerns are tangled; do not split speculatively.
Trade-offs
Following the principle buys isolated changes, simpler tests and reusable pieces, at the cost of more classes and some wiring. Over-applying it scatters cohesive logic. The judgement call is always the same question: do these parts change for the same reason, at the request of the same people?
In an interview
Apply it out loud when a class starts doing too much: "ParkingLot is getting pricing, ticket printing and spot allocation. Pricing changes with business rules and printing with hardware, so I will pull out a PricingPolicy and a TicketPrinter and keep ParkingLot responsible for spots." Naming the reason for each split is what scores.
Check yourself
A UserService validates sign-up data, hashes passwords, writes the user to the database, and sends a welcome email. A change to the email template breaks sign-ups in production. Which refactoring best follows the single responsibility principle?