Simplifying Code with the Single Responsibility Principle
A class should have one, and only one, reason to change. We explain the 'S' in SOLID with a simpl...
Abstract AlgorithmsTLDR: The Single Responsibility Principle says a class should have only one reason to change. If a change in DB schema AND a change in email format both require you to edit the same class, that class has two responsibilities โ and needs to be split.
๐ The Filing Cabinet That Also Sends Emails
Imagine a filing clerk whose job is:
- Store documents (filing cabinet logic).
- Send notification emails whenever a document is filed.
- Log activity to an audit trail.
One person doing three jobs โ fine until they're on vacation and you only need the email changed. You have to find and understand the whole multi-responsibility class just to change one notification template.
SRP says: each class has one job. One reason to put it in "maintenance mode."
๐ข The Classic Violation: User Manager Who Does Everything
// โ SRP violation โ two very different reasons to change this class
class UserManager {
public void saveUser(User user) {
// 1. save to DB
db.execute("INSERT INTO users VALUES (?)", user.data());
// 2. send a welcome email
emailService.send(user.email, "Welcome to our platform!", WELCOME_TEMPLATE);
// 3. write audit log
logger.info("User created: " + user.getId());
}
}
Reasons this class must change:
- The database schema changes โ modify DB logic.
- The welcome email template changes โ modify email logic.
- The audit log format changes โ modify logging logic.
Three reasons to change = three responsibilities.
โ๏ธ The SRP Fix: One Class, One Job
// โ
Split into single-responsibility classes
class UserRepository {
public void save(User user) {
db.execute("INSERT INTO users VALUES (?)", user.data());
}
}
class WelcomeEmailService {
public void sendWelcome(User user) {
emailService.send(user.email, "Welcome!", WELCOME_TEMPLATE);
}
}
class UserAuditLogger {
public void logCreated(User user) {
logger.info("User created: " + user.getId());
}
}
// Orchestrator โ knows when to call each, but not how they work
class UserRegistrationService {
private final UserRepository repo;
private final WelcomeEmailService emailSvc;
private final UserAuditLogger audit;
public void register(User user) {
repo.save(user);
emailSvc.sendWelcome(user);
audit.logCreated(user);
}
}
Now each class has exactly one reason to change. UserRegistrationService orchestrates the flow but owns none of the individual mechanics.
๐ง How to Spot an SRP Violation
Common signals:
| Signal | Example |
| Class name contains "And" | FileReaderAndParser, UserManagerAndNotifier |
| More than ~200 lines in a single class | Logic has grown without boundaries |
| Unit test needs many unrelated mocks | new UserManager(mockDB, mockEmailService, mockLogger, mockMetrics, ...) |
| Change in one feature breaks a test for another | Updating DB logic fails email tests |
| Merge conflicts between teammates on the same class | Two teams editing the same file for unrelated features |
โ๏ธ SRP vs. Cohesion: The Right Balance
SRP is sometimes misunderstood as "one method per class." That's wrong.
Cohesion is the right mental model: group methods that change together and depend on the same data. A User class can have getFullName(), getEmail(), and isActive() โ all relate to the same entity and would change for the same reasons.
| Too granular (over-SRP) | Appropriately SRP | Too coarse |
UserFirstNameGetter, UserLastNameGetter | User (all core user properties) | UserManagerNotifierLogger |
EmailValidator, EmailLengthChecker | EmailValidator (all validation rules) | UserEmailAndPermissionClass |
Rule of thumb: Ask "If the business rule changes for X, what code must change?" Group that code together.
๐ Summary
- A class should have one reason to change โ one axis of responsibility.
- The "And" smell in class names, oversized test setUp, and frequent merge conflicts all signal SRP violations.
- Split responsibilities into focused classes; use an orchestrator to compose them.
- Don't over-SRP: cohesion means grouping code that changes together โ not one method per class.
๐ Practice Quiz
A
PaymentServiceclass handles charging the card, sending a receipt, and updating the order status. How many responsibilities does it have?- A) One โ it's all payment-related.
- B) Three โ charging, notifying, and updating order state are separate responsibilities with different reasons to change.
- C) Two โ charging and notification are the split.
Answer: B
What is the clearest signal that a unit test violates SRP at the class level?
- A) The test has more than 10 assertions.
- B) The test's setUp requires mocking 5+ unrelated dependencies โ the class under test collaborates with too many different concerns.
- C) The test method name is too long.
Answer: B
A
Userclass hasgetId(),getEmail(),getFullName(), andisActive(). Does this violate SRP?- A) Yes โ it has four methods, each a separate responsibility.
- B) No โ all methods describe user state, change for the same reasons, and are highly cohesive.
- C) Yes โ
getFullName()should be in a separateNameFormatterclass.
Answer: B

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
SFT for LLMs: A Practical Guide to Supervised Fine-Tuning
TLDR: Supervised fine-tuning (SFT) is the stage where a pretrained model learns task-specific response behavior from curated input-output examples. It is usually the first alignment step after pretraining and often the foundation for later RLHF. Good...
RLHF in Practice: From Human Preferences to Better LLM Policies
TLDR: Reinforcement Learning from Human Feedback (RLHF) helps align language models with human preferences after pretraining and SFT. The typical pipeline is: collect preference comparisons, train a reward model, then optimize a policy (often with KL...
PEFT, LoRA, and QLoRA: A Practical Guide to Efficient LLM Fine-Tuning
TLDR: Full fine-tuning updates every model weight, which is expensive in memory, compute, and storage. PEFT methods update only a small trainable slice. LoRA learns low-rank adapters on top of frozen base weights. QLoRA pushes efficiency further by q...
LLM Model Naming Conventions: How to Read Names and Why They Matter
TLDR: LLM names encode practical decisions: model family, size, training stage, context window, format, and quantization level. If you can decode naming conventions, you can avoid costly deployment mistakes and choose the right checkpoint faster. ๏ฟฝ...
