Skip to main content

Single Responsibility Principle

What Is the Single Responsibility Principle?

The Single Responsibility Principle (SRP) states that a class should have only one reason to change. In other words, a class should have one job or responsibility. This principle encourages cohesion—grouping related functionality together—while reducing coupling between different concerns.

When a class has multiple responsibilities, changes to one responsibility can break the other. For example, a User class that handles both user data persistence and email notifications violates SRP. If you need to change how emails are sent, you must modify the User class, risking unintended side effects on user persistence logic.

TL;DR

The Single Responsibility Principle requires that each class handle exactly one concern. Changes to database logic shouldn't touch business logic. Use extraction and composition to separate distinct responsibilities. SRP improves testability, reusability, and maintainability while reducing ripple effects from changes.

Learning Objectives

You will be able to:

  • Identify multiple responsibilities hidden within a single class
  • Refactor classes to follow SRP through extraction and composition
  • Understand the connection between SRP and change propagation
  • Recognize common SRP violations in legacy code
  • Apply SRP in real-world scenarios with proper abstraction

Motivating Scenario

Your team maintains a legacy OrderProcessor class that handles order validation, database persistence, payment processing, and email notifications. When the payment gateway changes, the class needs modification, risking bugs in order persistence. When email templates change, the payment logic unexpectedly breaks. The OrderProcessor has four reasons to change, making it fragile and hard to test in isolation.

By applying SRP, you extract each responsibility into focused classes: OrderValidator, OrderRepository, PaymentGateway, and EmailNotifier. Now changes to payment logic don't affect email functionality, and each class can be tested independently with clear dependencies.

Core Concepts

Understanding "Reason to Change"

A "reason to change" isn't just code modification—it's a requirement change. If stakeholders might request changes to:

  • Business logic for order validation
  • How orders persist to the database
  • Which payment processor to use
  • Email notification timing

...then you have multiple reasons to change, indicating multiple responsibilities.

Identifying reasons to change in a violation vs. compliant design

The Cohesion-Coupling Balance

SRP works in tandem with cohesion and coupling:

  • Cohesion: How closely related are methods within a class? High cohesion groups similar functionality.
  • Coupling: How dependent are classes on each other? Low coupling reduces ripple effects.

When you apply SRP, you increase cohesion within classes and often decrease coupling by making dependencies explicit.

When Extraction Isn't the Answer

SRP doesn't mean every method should be in its own class. A Point class with x and y coordinates has one responsibility: representing a point. Adding angle calculation, rotation, or distance methods still serves the single responsibility of modeling point geometry—they're all aspects of that one concern.

Practical Example

Let me show you a violation and then refactor it properly.

BEFORE (SRP Violation):

class User:
def __init__(self, username, email):
self.username = username
self.email = email

def save_to_database(self):
# Database logic mixed with business logic
connection = create_db_connection()
query = f"INSERT INTO users (username, email) VALUES ('{self.username}', '{self.email}')"
connection.execute(query)

def send_welcome_email(self):
# Email logic mixed in
smtp = create_smtp_connection()
message = f"Welcome {self.username}!"
smtp.send(self.email, message)

def validate(self):
# Validation logic
return "@" in self.email and len(self.username) > 2

AFTER (SRP Compliant):

class User:
"""Represents user data—single responsibility."""
def __init__(self, username, email):
self.username = username
self.email = email

class UserValidator:
"""Validates user input."""
def validate(self, user):
return "@" in user.email and len(user.username) > 2

class UserRepository:
"""Handles user persistence."""
def save(self, user):
connection = create_db_connection()
query = f"INSERT INTO users (username, email) VALUES ('{user.username}', '{user.email}')"
connection.execute(query)

class EmailService:
"""Handles email notifications."""
def send_welcome_email(self, user):
smtp = create_smtp_connection()
message = f"Welcome {user.username}!"
smtp.send(user.email, message)

# Usage
user = User("alice", "alice@example.com")
validator = UserValidator()
if validator.validate(user):
repository = UserRepository()
repository.save(user)
email_service = EmailService()
email_service.send_welcome_email(user)

Patterns & Pitfalls

Pitfall: Over-Fragmentation

Don't extract a responsibility for every method. A UserRepository with save(), update(), and delete() is appropriate—they're all persistence operations. Extracting each into separate classes creates unnecessary complexity.

Pattern: Dependency Injection

SRP becomes powerful with dependency injection. Instead of UserRepository creating its own database connection, inject it:

class UserRepository:
def __init__(self, db_connection):
self.db = db_connection

def save(self, user):
# Use injected connection
self.db.execute(...)

This makes testing easier: inject a mock connection in tests.

Pitfall: Feature Envy

Watch for methods accessing many properties of another object. This signals that logic should move to the object being queried. If OrderProcessor.calculateTax() accesses order.items, order.shipping, order.location, that calculation belongs in Order, not OrderProcessor.

When to Use / When Not to Use

Use SRP when:

  • Classes have multiple reasons to change
  • Testing requires mocking multiple concerns
  • Teams will independently modify different parts (payment, validation, storage)
  • You're building frameworks or libraries with extensibility needs

Avoid over-applying SRP when:

  • Creating classes so small they become noise
  • Building simple scripts or prototypes where cohesion is obvious
  • The responsibility is truly inseparable in practice
  • Your team is small and understands the coupling

Design Review Checklist

  • Each class has one clear reason to change
  • Database logic is isolated from business logic
  • Validation logic is separate from persistence
  • Email/notification logic doesn't couple to core domain
  • External service integration (APIs, payment gateways) is abstracted
  • Tests for each class don't require mocking unrelated components
  • No class reads many properties from another (feature envy)
  • High cohesion: methods in a class are closely related
  • Clear naming explains the single responsibility

Self-Check

  1. In your current project, identify a class with 4+ public methods. How many distinct reasons might it have to change?
  2. Write down which external systems your User or core entity class depends on. Do these dependencies belong there?
  3. Can you test one responsibility without setting up all others (database, email, payment)?

Next Steps

One Takeaway

note

A class with one responsibility is easier to understand, test, and modify. When requirements change, you modify less code and break fewer things. Invest upfront in separation to pay dividends in maintenance.

References

  1. Uncle Bob: The Single Responsibility Principle ↗️
  2. Wikipedia: Single-Responsibility Principle ↗️