Detail chart · Backend

Single Responsibility

Principle ◆◇◇◇◇

Every module, class, or function should have one reason to change — separating concerns produces systems that are easier to reason about.

Summary

The Single Responsibility Principle (SRP) states that a unit of code — function, class, module — should encapsulate one concern and have only one reason to change. When a component owns multiple concerns, changes to one can inadvertently break the other, making the system harder to evolve safely.

Problem

Classes and functions that do too much become magnets for unrelated changes. A UserService that handles authentication, profile updates, email notifications, and audit logging will be modified by four different teams for four different reasons — each change carrying risk to the others.

Solution

Identify distinct reasons a unit might change — different business concerns, different rates of change, different stakeholders — and split them into separate units.

// Too many responsibilities
class UserService {
  authenticate(credentials) { ... }
  updateProfile(user, data) { ... }
  sendWelcomeEmail(user) { ... }       // email concern
  logAuditEvent(user, action) { ... }  // audit concern
}

// Separated responsibilities
class AuthService      { authenticate(credentials) { ... } }
class UserProfileService { updateProfile(user, data) { ... } }
class EmailService     { sendWelcomeEmail(user) { ... } }
class AuditService     { logEvent(user, action) { ... } }

Each service can now evolve, be tested, and be deployed independently.

The same split holds regardless of language — here it is in Python, where the “and” in a class name is just as reliable a smell:

# Too many responsibilities
class UserService:
    def authenticate(self, credentials): ...
    def update_profile(self, user, data): ...
    def send_welcome_email(self, user): ...   # email concern
    def log_audit_event(self, user, action): ...  # audit concern


# Separated responsibilities
class AuthService:
    def authenticate(self, credentials): ...

class UserProfileService:
    def update_profile(self, user, data): ...

class EmailService:
    def send_welcome_email(self, user): ...

class AuditService:
    def log_event(self, user, action): ...

Live Playground

Experiment with the pattern below. In the before version, one UserService owns credentials, profile storage, the welcome email, and the audit trail. Marketing changes the email greeting, and that change breaks registration: the user is never saved and the audit entry is never written. In the after version, each concern is its own unit. The same template change is caught by a test that constructs only EmailService, and during registration the failure stays inside the notification step.

When to Use

Avoid when:

Trade-offs

BenefitCost
Changes are localized — lower risk of unintended side effectsMore files and classes to navigate
Each unit is independently testableIncreased coordination between components at call sites
Clear ownership — one team/domain per componentCan lead to over-engineering if applied too early
Code is easier to reason about in isolationRequires good naming and organization to avoid confusion