Early Access: 87 spots left.

Claim

Course content

No content available

The Single Responsibility Principle says that a class should have one, and only one, reason to change. If you can think of two different events that would each force you to edit the same class, the class is taking on more than one responsibility.

A class that does too much accumulates merges and bugs. Two unrelated teams end up editing the same file. Tests grow unwieldy because the class needs every collaborator it touches set up. Splitting the responsibilities into smaller classes keeps each one focused, easier to test, and easier to change without breaking unrelated features.

This module walks a single codebase through all five SOLID principles: the checkout backend of an online store that registers customers, takes payments, tracks store-credit balances, and records orders. The whole tour is in SOLID in a real project. This principle's slice is UserService, the class that registers a new customer. You do not need the tour to follow along; everything below stands on its own.

SRP is not about counting lines or methods. A class with a single method can still violate SRP if that method does several unrelated things. A class with twenty methods can be fine if all twenty operate on the same concept.

The right question to ask is: what causes this class to change? If the answer involves multiple unrelated concerns, such as database schema, email format, and report layout, the class is doing too much. Each concern should live in its own class so that a change to one does not touch the others.

SmellWhat it looks likeWhy it's a problem
Class name uses "And" or "Manager"UserManager, OrderAndPaymentServiceThe name itself admits multiple concerns
Methods cluster into unrelated groupssave(), sendEmail(), formatHtml() in one classEach cluster has its own reason to change
A method does several things in sequence"Save the user, then email them, then log it"Three unrelated concerns are bound together
Tests need a long setupMock DB, mock mailer, and mock logger to test one methodThe class depends on too many things to do its single job
Different teams keep editing the same fileThe persistence team and the notification team both touch UserServiceEach team's concern is buried in the same class

UserService above has three reasons to change. If the database schema changes, this class changes. If the wording of the welcome email changes, this class changes. If the audit format changes, this class changes. None of those concerns have anything to do with each other, but they are all glued into a single method.

The consequence is that a copy-edit on the welcome email forces a rebuild and redeploy of the persistence path. Anyone reviewing a change here has to think about all three concerns at once. Tests need to set up a real database, a real mailer, and a real logger just to verify a single registration.

What this costs in production

A copywriter asked to change the wording of the welcome email. Because the email text lived inside UserService.registerUser next to the database write, the one-word change forced a rebuild and redeploy of the user persistence path. The deploy went out on a Friday, an unrelated change in the same class had a bug, and a copy-edit took down user registration for an hour.

After the split, UserService is a thin orchestrator. Its only job is to know that registering a user means "save, then notify, then audit". The actual saving lives in UserRepository. The actual sending lives in EmailNotifier. The actual logging lives in AuditLogger.

A schema migration now touches UserRepository and nothing else. A wording change to the welcome email touches EmailNotifier and nothing else. The persistence team and the notification team can edit their own files without merge conflicts. Tests for UserService can pass in fakes for the three collaborators, so a registration test no longer needs a real database or a real mail server.

savesendWelcomerecordUserServiceUserRepositoryEmailNotifierAuditLogger
UserService runs one step each on three focused collaborators. Rewording the welcome email now changes only EmailNotifier; the persistence and audit paths are untouched and need no retest.

Everything so far has been about one class, but the principle is a question you can ask of any unit of code: who asks for changes to this? Robert C. Martin's precise phrasing is that a module should be responsible to one, and only one, actor. In the smelly UserService, three actors were pulling at the same file: the copywriter owns the email wording, the database team owns the schema, and the compliance team owns the audit format.

The same question works one level down. A method that validates input, saves a row, and sends an email in sequence is carrying three jobs in one body, even if its class is otherwise clean. Extracting each step into its own well-named method is this principle at method scale, and it is most of what code reviewers mean when they say a function should do one thing.

It also works levels up. A utils package is the module-scale version of a class named Manager: a bucket with no single reason to change, which is why every team edits it and it only ever grows. At service scale, the reason microservice boundaries are usually drawn around business capabilities is exactly this principle. One service, one owning team, one stream of change; when two teams keep deploying the same service for unrelated reasons, that service is a UserService writ large.

Six months after the refactor, marketing rebrands the product and rewrites the welcome email: new copy, an HTML template, a promotional banner. In the original design that edit lands inside UserService.registerUser, two lines from the database write, and it redeploys the whole class. The war story above is that exact release going wrong.

In the refactored design, the entire change is confined to one small file.

What this saves in production

The blast radius of a copy change is now one class with no database access. The persistence and audit paths ship untouched, so the failure mode from the war story, a copy-edit taking down registration, is structurally gone. The review is small enough to actually read, and the only tests that must run are EmailNotifier's own.

A common mistake is to extract every piece of logic into its own class. If a class genuinely has one responsibility, leave it alone. The point of SRP is to align the structure of your code with the way changes actually happen in your project. If two pieces of logic always change together, they belong in the same class.

A good rule of thumb is to wait for the second reason to change before splitting. Until you have evidence that two concerns evolve independently, keep them together. Splitting prematurely costs you indirection without buying any flexibility.

The companion exercise gives you a smelly UserService that mixes persistence, notification, and report formatting. Refactor it so that each concern lives in its own class. The validator runs five checks that only pass if you actually applied SRP, including a reflection check that prevents you from copying the same code into every class.

Back to Low Level Design