One reason to change, not one thing
The formulation that survives contact with real code is Robert C. Martin's: a class should have one reason to change. Later restatements sharpen it into "gather together the things that change for the same reasons, and separate those that change for different reasons", which is the version worth carrying into an interview because it is falsifiable. "Does one thing" is not falsifiable, since any method can be described as one thing at a sufficiently vague altitude. Saving an invoice, emailing an invoice, and pricing an invoice are all "handling invoices" if you squint.
So the diagnostic is not how many methods a class has. It is how many independent forces can arrive and demand an edit to that file.
The class
Here is a shape that shows up in nearly every billing codebase eventually.
class Invoice {
long id;
String customerEmail;
BigDecimal amount;
LocalDate dueDate;
boolean paid;
BigDecimal totalWithLateFee() {
if (paid || !LocalDate.now().isAfter(dueDate)) return amount;
long daysLate = ChronoUnit.DAYS.between(dueDate, LocalDate.now());
BigDecimal periods = BigDecimal.valueOf(daysLate / 30 + 1);
BigDecimal rate = new BigDecimal("0.015").multiply(periods)
.min(new BigDecimal("0.10"));
return amount.add(amount.multiply(rate));
}
String toReminderHtml() {
return "<p>Invoice #" + id + " for " + totalWithLateFee()
+ " was due on " + dueDate + ".</p>";
}
void save(Connection c) throws SQLException {
var ps = c.prepareStatement(
"UPDATE invoice SET amount=?, due_date=?, paid=? WHERE id=?");
ps.setBigDecimal(, amount);
ps.setDate(, Date.valueOf(dueDate));
ps.setBoolean(, paid);
ps.setLong(, id);
ps.executeUpdate();
}
}
Three requesters, one file
The reason this is a genuine violation rather than an aesthetic complaint is visible in version control. Run git log --oneline over the file and you get commits titled "late fee cap raised to 12%", "reminder email restyled for the new brand", and "add invoice.currency column". Three different people, answering to three different stakeholders, editing one file for reasons that have nothing to do with each other.
That is what makes the class expensive. The finance change and the branding change collide in review. A schema migration forces a retest of fee arithmetic because the tests for both live in InvoiceTest. And you cannot compute a late fee in a unit test without either a live Connection on the classpath or an object that is half-constructed, because the persistence concern has pulled infrastructure into the same type as the arithmetic.
The refactor
Split along those three axes and nothing else.
Two things improved beyond tidiness. asOf is now a parameter rather than a hidden call to LocalDate.now(), so fee behaviour at a month boundary is testable without freezing the clock. And LateFeePolicy is the piece finance actually asks about, so their change now lands in a file whose entire test suite is about fees.
Why "does one thing" is the wrong test
The candidates who lose points here are the ones who apply the slogan mechanically and split by verb. You get InvoiceValidator, InvoiceCalculator, InvoiceFormatter, InvoiceSaver, InvoiceLoader, each with a single static-ish method, and an InvoiceManager that calls them in order. Every one of those classes "does one thing", and the design is worse than what you started with, because the invoice is now a bag of fields with no behaviour and the real logic has migrated into a procedural script wearing class syntax.
The test that works is the requester test. If you cannot name two distinct people or roles who would ask for two changes to this class, you do not yet have evidence of two responsibilities, and splitting is speculation. Two methods serving the same stakeholder for the same reason belong together, however different they look.
Where SRP taken literally becomes the problem
Be honest that the principle has no natural stopping point. Pushed all the way, it says every reason to change deserves its own type, and the endpoint of that is a codebase where following one request means opening nine files and no single file explains anything. The indirection is not free either: an interface with exactly one implementation and no plausible second one is a layer you pay to read forever in exchange for flexibility you never use.
The counterweight is cohesion. SRP is one half of a pair, and the other half is that a class should be worth existing. In practice that means splitting on churn you have observed rather than churn you can imagine, extracting the second implementation when it arrives rather than in anticipation, and accepting that a 120-line class with two related responsibilities can be the correct trade-off in a service two people maintain. Saying that out loud reads as judgement, not as ignorance of the principle.
The strong version of this answer is not the refactor, which any candidate can produce. It is naming which stakeholder owns each extracted piece, and then naming the point at which you would refuse to split further.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).