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".