SOLID Principles โข S
Single Responsibility Principle
A class should have only one reason to change. The most fundamental of the SOLID principles and the foundation of clean code.
10 min readEssential
1The Core Idea
๐ฏ Real-World Analogy
๐จโ๐ณ
Restaurant Kitchen
In a well-run kitchen, you have: Chef (cooks), Dishwasher (cleans dishes), Waiter (serves customers), Cashier (handles payments). Each person has ONE job.
๐ง
Swiss Army Knife Trap
A Swiss Army knife does many things... but none of them well. A chef's knife does ONE thing - cutting - and does it excellently.
Single Responsibility Principle (SRP): A class should have only one reason to change. In other words, a class should have only one job, one purpose, one responsibility.
"One Reason to Change" Explained
โ Multiple Reasons to Change
โข Change email format โ Modify class
โข Change database โ Modify class
โข Change validation โ Modify class
3 different teams might need to change this class!
โ
Single Reason to Change
โข EmailService - only email changes
โข UserRepository - only DB changes
โข UserValidator - only validation changes
Each class has ONE owner!
2SRP Violation (Bad Code)
Let's look at a common SRP violation - a class that does too many things:
โ SRP Violation - God Class
// โ BAD: This class has MULTIPLE responsibilities!
public class UserManager {
// Responsibility 1: User data management
private String name;
private String email;
// Responsibility 2: Database operations
public void saveToDatabase() {
// Connect to database
Connection conn = DriverManager.getConnection("jdbc:mysql://...");
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO users (name, email) VALUES (?, ?)"
);
stmt.setString(1, this.name);
stmt.setString(2, this.email);
stmt.executeUpdate();
// If database changes (MySQL โ PostgreSQL), this class changes!
}
// Responsibility 3: Email notifications
public void sendWelcomeEmail() {
// Set up email configuration
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com");
Session session = Session.getInstance(props);
Message msg = new MimeMessage(session);
msg.setSubject("Welcome!");
msg.setText("Hello " + this.name + ", welcome to our platform!");
Transport.send(msg);
// If email provider changes, this class changes!
}
// Responsibility 4: Input validation
public boolean validateEmail() {
return email != null && email.contains("@") && email.contains(".");
// If validation rules change, this class changes!
}
// Responsibility 5: Password hashing
public String hashPassword(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt());
// If hashing algorithm changes, this class changes!
}
// Responsibility 6: Logging
public void logUserActivity(String activity) {
System.out.println(LocalDateTime.now() + ": " + name + " - " + activity);
// If logging format changes, this class changes!
}
}
/*
* PROBLEMS:
* 1. This class has 6 different reasons to change!
* 2. Any team (DB, Email, Security, Logging) might modify it
* 3. Hard to test - need to mock DB, email server, etc.
* 4. Changes ripple through the entire class
* 5. Violates separation of concerns
*/Why is this bad?
๐ Multiple Change Reasons:
- Database team changes DB
- DevOps changes email service
- Security team updates hashing
- UX changes validation rules
๐ฅ Consequences:
- High risk of breaking things
- Merge conflicts between teams
- Hard to test in isolation
- Difficult to reuse parts
3Following SRP (Good Code)
Let's refactor the god class into separate, focused classes:
Refactored Class Structure
User
Hold user data
UserRepository
Database operations
EmailService
Send emails
UserValidator
Validate input
PasswordHasher
Hash passwords
ActivityLogger
Log activities
Each class has ONE responsibility and ONE reason to change
โ
Following SRP - Separate Responsibilities
// โ
GOOD: Each class has ONE responsibility
// 1. User - Only holds user data (Data Transfer Object)
public class User {
private final String name;
private final String email;
private final String passwordHash;
public User(String name, String email, String passwordHash) {
this.name = name;
this.email = email;
this.passwordHash = passwordHash;
}
// Only getters - just data, no behavior
public String getName() { return name; }
public String getEmail() { return email; }
public String getPasswordHash() { return passwordHash; }
}
// 2. UserValidator - Only validates user input
public class UserValidator {
public boolean isValidEmail(String email) {
return email != null &&
email.contains("@") &&
email.contains(".") &&
email.length() > 5;
}
public boolean isValidPassword(String password) {
return password != null && password.length() >= 8;
}
public boolean isValidName(String name) {
return name != null && !name.trim().isEmpty();
}
}
// 3. PasswordHasher - Only hashes passwords
public class PasswordHasher {
public String hash(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt());
}
public boolean verify(String password, String hash) {
return BCrypt.checkpw(password, hash);
}
}
// 4. UserRepository - Only handles database operations
public class UserRepository {
private final DataSource dataSource;
public UserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public void save(User user) {
try (Connection conn = dataSource.getConnection()) {
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)"
);
stmt.setString(1, user.getName());
stmt.setString(2, user.getEmail());
stmt.setString(3, user.getPasswordHash());
stmt.executeUpdate();
}
}
public User findByEmail(String email) {
// Query database and return User
}
}
// 5. EmailService - Only sends emails
public class EmailService {
private final String smtpHost;
private final String fromAddress;
public EmailService(String smtpHost, String fromAddress) {
this.smtpHost = smtpHost;
this.fromAddress = fromAddress;
}
public void sendWelcomeEmail(User user) {
String subject = "Welcome!";
String body = "Hello " + user.getName() + ", welcome to our platform!";
sendEmail(user.getEmail(), subject, body);
}
private void sendEmail(String to, String subject, String body) {
// Email sending logic
}
}
// 6. ActivityLogger - Only logs activities
public class ActivityLogger {
public void log(User user, String activity) {
System.out.println(
LocalDateTime.now() + " [" + user.getEmail() + "] " + activity
);
}
}
// 7. UserService - Orchestrates the workflow (Facade)
public class UserService {
private final UserValidator validator;
private final PasswordHasher hasher;
private final UserRepository repository;
private final EmailService emailService;
private final ActivityLogger logger;
public UserService(UserValidator validator, PasswordHasher hasher,
UserRepository repository, EmailService emailService,
ActivityLogger logger) {
this.validator = validator;
this.hasher = hasher;
this.repository = repository;
this.emailService = emailService;
this.logger = logger;
}
public User registerUser(String name, String email, String password) {
// Validate
if (!validator.isValidEmail(email)) {
throw new IllegalArgumentException("Invalid email");
}
if (!validator.isValidPassword(password)) {
throw new IllegalArgumentException("Invalid password");
}
// Create user with hashed password
String hash = hasher.hash(password);
User user = new User(name, email, hash);
// Save to database
repository.save(user);
// Send welcome email
emailService.sendWelcomeEmail(user);
// Log activity
logger.log(user, "User registered");
return user;
}
}4Benefits of Following SRP
๐งช
Easy to Test
Each class can be unit tested in isolation. Mock dependencies easily.
๐ง
Easy to Maintain
Changes to validation don't affect email logic. Bug fixes are localized.
โป๏ธ
Reusable Components
EmailService can be used anywhere. PasswordHasher works for any feature.
๐ฅ
Team Friendly
Different teams can work on different classes without conflicts.
๐
Readable Code
Class names describe exactly what they do. Self-documenting code.
๐
Easy to Extend
Add new features without modifying existing classes (Open/Closed).
5How to Identify SRP Violations
Use these heuristics to spot classes that violate SRP:
โ ๏ธ Class name includes 'And' or 'Manager' or 'Handler'
Example: UserAndOrderManager, DataHandler
Fix: Split into User and Order classes
โ ๏ธ Class has methods that don't use each other
Example: validateEmail() and saveToDatabase() share nothing
Fix: Separate into Validator and Repository
โ ๏ธ Class changes for different reasons
Example: DB schema change AND email format change both require modifying the class
Fix: Extract into separate classes
โ ๏ธ Class imports unrelated libraries
Example: Imports both SMTP and MySQL libraries
Fix: Create EmailService and UserRepository
โ ๏ธ Class has more than 200-300 lines
Example: God class with 1000+ lines
Fix: Extract cohesive functionality into separate classes
๐ก The 'Describe in One Sentence' Test
Can you describe what your class does in one sentence WITHOUT using "and" or "or"? If not, it probably has multiple responsibilities.
6Common Mistakes
โ Taking SRP too far
Problem: Creating classes with just one method (over-engineering)
Solution: Group related operations. A UserRepository can have save, update, delete methods.
โ Confusing 'responsibility' with 'method'
Problem: Thinking each method needs its own class
Solution: Responsibility = reason to change, not individual operations.
โ Ignoring cohesion
Problem: Splitting tightly related logic into separate classes
Solution: Keep related logic together. User data + User validation might be okay together.
โ Not considering business context
Problem: Splitting based on technical layers only
Solution: Consider who requests changes. Sometimes business grouping makes more sense.
7Interview Questions
โ What is the Single Responsibility Principle?
SRP states that a class should have only one reason to change. It should have one job, one purpose. This means that if you need to change the class, it should be because of one specific type of change, not multiple different types.
โ How do you identify an SRP violation?
Look for: 1) Classes with 'And' or 'Manager' in the name, 2) Methods that don't use each other's data, 3) Multiple teams needing to modify the same class, 4) Class importing unrelated libraries, 5) Class changing for different reasons.
โ What's the benefit of following SRP?
Benefits include: easier testing (isolated units), easier maintenance (changes are localized), better reusability (components can be used elsewhere), team-friendly (less merge conflicts), and improved readability (classes have clear purposes).
โ Can you give an example of refactoring for SRP?
A UserManager that handles validation, database, and email can be split into: User (data), UserValidator (validation rules), UserRepository (database operations), EmailService (notifications). Each class then has one clear responsibility.
8Key Takeaways
1SRP: A class should have only ONE reason to change.
2Reason to change = who requests changes (DB team, Security team, etc.).
3Signs of violation: "And" in class name, unrelated imports, methods that don't share data.
4Benefits: Easier testing, maintenance, reusability, and team collaboration.
5Don't over-engineer: related operations can stay together.
6Use the "one sentence" test: describe the class without "and"/"or".