Single Responsibility Principle (SRP)

A class should have one and only one reason to change. The smell, the refactor, the cost of misapplying it.

Concept Foundational
7 min read
solid srp cohesion refactoring

Summary#

The Single Responsibility Principle is the S in SOLID and the easiest of the five to state, easiest to misquote, and easiest to over-apply. Robert Martin’s formulation: a class should have one, and only one, reason to change.

The trap in the principle is the word responsibility. Read naively, it suggests “each class should do exactly one thing” — which, taken literally, ends in a thousand single-method classes and no working system. The actual principle is about axes of change: a class should answer to a single set of stakeholders, a single source of requirements, a single force that pulls it in a direction. When two unrelated forces pull on the same class, the class will be edited for two unrelated reasons, by two unrelated teams, and the merge conflicts will write the case for splitting it.

Why it matters#

SRP is the discipline that produces small, comprehensible classes — and small, comprehensible classes are what the next four SOLID principles assume. None of OCP, LSP, ISP, or DIP can be applied cleanly on top of a god class.

Three concrete reasons interviewers care:

  • Test surface stays tractable. A class with one responsibility has tests that target one set of inputs and one set of effects. A class with three responsibilities has tests whose setup is a tangle of fixtures from three subsystems.
  • Coupling shrinks. A User class that handles persistence, validation, and email delivery imports the database driver, the validation library, and the SMTP client — and every caller transitively depends on all three.
  • Change locality improves. When the password-policy team tightens their rules, only PasswordPolicy should change. If their change forces a redeploy of UserDataAccess because the two share a class, the architecture is fighting the team structure.

The principle’s value is measured at the diff level: how many unrelated files does a single ticket touch? SRP is what keeps that number small.

How it works#

Spotting the violation#

The most reliable detector is the reasons-to-change question. Pick a class. List the stakeholder roles that could ask for a change to it. If the list is longer than one, you have an SRP violation in flight.

A worked example. Consider an early draft of a UserService:

public final class UserService {
private final Database db;
private final SmtpClient smtp;
private final PasswordHasher hasher;
public UserService(Database db, SmtpClient smtp, PasswordHasher hasher) {
this.db = db; this.smtp = smtp; this.hasher = hasher;
}
public void register(String email, String password) {
if (!email.contains("@")) throw new IllegalArgumentException("Bad email");
if (password.length() < 8) throw new IllegalArgumentException("Password too short");
String hash = hasher.hash(password);
db.insertUser(email, hash);
String body = "Welcome to the service, " + email + "!\nPlease confirm at /confirm";
smtp.send(email, "Welcome", body);
}
}

The class is doing four things, each answering to a different stakeholder:

  1. Validation — answers to product (what’s a valid email; how long must passwords be).
  2. Hashing policy — answers to security.
  3. Persistence — answers to the data team (which database, which schema).
  4. Notification copy and delivery — answers to marketing and the messaging team.

A change driven by any one of them forces redeploys for all of them. That is the smell SRP names.

The refactor#

Split along the axes of change. Each new class has exactly one stakeholder it must please:

public final class UserValidator {
public void validate(String email, String password) {
if (!email.contains("@")) throw new IllegalArgumentException("Bad email");
if (password.length() < 8) throw new IllegalArgumentException("Password too short");
}
}
public final class UserRepository {
private final Database db;
public UserRepository(Database db) { this.db = db; }
public void save(String email, String passwordHash) { db.insertUser(email, passwordHash); }
}
public final class WelcomeMailer {
private final SmtpClient smtp;
public WelcomeMailer(SmtpClient smtp) { this.smtp = smtp; }
public void sendWelcome(String email) {
String body = "Welcome to the service, " + email + "!\nPlease confirm at /confirm";
smtp.send(email, "Welcome", body);
}
}
public final class UserRegistration {
private final UserValidator validator;
private final UserRepository users;
private final PasswordHasher hasher;
private final WelcomeMailer mailer;
public UserRegistration(UserValidator v, UserRepository r, PasswordHasher h, WelcomeMailer m) {
this.validator = v; this.users = r; this.hasher = h; this.mailer = m;
}
public void register(String email, String password) {
validator.validate(email, password);
users.save(email, hasher.hash(password));
mailer.sendWelcome(email);
}
}

UserRegistration is now a coordinator — its single reason to change is “the registration flow itself was redesigned.” Each collaborator has its own reason. Marketing can rewrite welcome copy without touching the repository. Security can change the hasher without touching the mailer. The data team can migrate the schema with the validator untouched.

The other side — when SRP has been over-applied#

The principle is also misused. The opposite failure mode is real and worth naming:

Under-applied (God class). One UserService with eight responsibilities. Edits collide. Tests are slow. Coupling is total.

Over-applied (Anaemic shred). EmailValidator, EmailNormaliser, EmailDomainExtractor, EmailFormatter, EmailTrimmer. Now the coordination between them is the complexity, and the call graph is a maze.

The signal that SRP has been over-applied: classes that are only ever called in a fixed sequence by a single coordinator, with no other caller. They are not separate responsibilities — they are steps. Inline them and let the coordinator hold the logic.

Variants and trade-offs#

A few useful refinements of the principle:

  • “Single reason to change” beats “single thing it does”. The reasons-to-change framing handles cases the literal one-thing reading mishandles. A Money class that knows how to add, subtract, compare, and format itself has one responsibility (it is the money concept) and many methods.
  • Stakeholder, not technical layer. Two methods that both touch the database may answer to two different stakeholders and belong in two different classes; two methods that touch the database and an email gateway may answer to one (the registration flow) and belong together as a coordinator.
  • “Cohesion” is SRP’s positive form. High cohesion is the same property phrased as “everything in this class is about the same thing.” If you cannot describe a class in one short noun phrase without listing alternatives, cohesion is low and SRP is at risk.
GranularityWhen it fitsRisk
Coarse — one class per use caseSmall, stable domains where forces are alignedDrifts toward a god class as the domain grows
Medium — split by axis of changeThe usual right answerRequires judgment — the axes are not always obvious up front
Fine — one class per methodPipelines with many distinct steps, or hot-swap behaviourCoordinator becomes the new god class; cognitive overhead grows

When this is asked in interviews#

Three moves carry SRP discussion in an LLD round:

  • Lead with reasons-to-change, not “one thing.” If the interviewer hears the literal form, they will push you on Money.add() and Money.format() — be ready to defend that those answer to the same stakeholder (the domain concept of money) and so live together.
  • Refactor under time pressure. Many interviewers will hand you a draft UserService like the one above and ask “what’s wrong?” Identify the four axes; sketch the four classes plus a coordinator; do not invent interfaces yet — that is DIP’s job.
  • Defend not splitting. Over-application is the more impressive miss to catch. If you propose splitting Money into MoneyAdder and MoneyFormatter, walk it back. Knowing where the principle ends is the senior signal.

The principle’s one durable test, after the meeting: would a change requested by team A also force a change to code owned by team B? If yes, the boundary is wrong.

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.