All Posts

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 AlgorithmsAbstract Algorithms
ยทยท4 min read
Share
Share on X / Twitter
Share on LinkedIn
Copy link

TLDR: 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:

  1. Store documents (filing cabinet logic).
  2. Send notification emails whenever a document is filed.
  3. 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:

SignalExample
Class name contains "And"FileReaderAndParser, UserManagerAndNotifier
More than ~200 lines in a single classLogic has grown without boundaries
Unit test needs many unrelated mocksnew UserManager(mockDB, mockEmailService, mockLogger, mockMetrics, ...)
Change in one feature breaks a test for anotherUpdating DB logic fails email tests
Merge conflicts between teammates on the same classTwo 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 SRPToo coarse
UserFirstNameGetter, UserLastNameGetterUser (all core user properties)UserManagerNotifierLogger
EmailValidator, EmailLengthCheckerEmailValidator (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

  1. A PaymentService class 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
  2. 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
  3. A User class has getId(), getEmail(), getFullName(), and isActive(). 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 separate NameFormatter class.
      Answer: B

Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms