Skip to content
LowLevelDesign Mastery

Single Responsibility Principle

A class should have only one reason to change.

In this chapter, we will explore the SOLID principles, a set of five design principles that help developers create maintainable and scalable software. These principles were introduced by Robert C. Martin and are widely regarded as best practices in object-oriented programming.

For starters, SOLID is an acronym that stands for:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

The Single Responsibility Principle (SRP) states that a software component (class, module, function, etc.) must have only one responsibility or one reason to change. This means that each component should be responsible for a single functionality or behavior.

If you find yourself modifying a class for different reasons, it’s a sign that the abstraction is incorrect and the class has too many responsibilities. This indicates that you need to create more objects to address the extra responsibilities.

Objects that do one thing, and just one thing, are:

  • Easier to maintain - Changes are isolated
  • Easier to understand - Clear purpose
  • Easier to test - Focused test cases
  • Less prone to bugs - Fewer side effects

We want to avoid “God Objects” that know too much or do too much.

Consider a user management system where a single class handles user data, validation, persistence, and email notifications. This violates SRP because the class has multiple reasons to change.

SRP violation: a single User class handling validation, database persistence, welcome email sending and password hashing
💡 Tip: Click dropdown to switch between languages
bad_user.py
import hashlib
class User:
def __init__(self, username: str, email: str, password: str):
self.username = username
self.email = email
self.password = password
def validate(self):
"""Responsibility 1: Validation"""
if not self.username:
raise ValueError("Username is required")
if "@" not in self.email:
raise ValueError("Invalid email")
return True
def save_to_database(self):
"""Responsibility 2: Database persistence"""
# Database connection and save logic
print(f"Saving {self.username} to database...")
def send_welcome_email(self):
"""Responsibility 3: Email notification"""
# Email sending logic
print(f"Sending welcome email to {self.email}...")
def hash_password(self):
"""Responsibility 4: Security"""
# Password hashing logic
self.password = hashlib.sha256(self.password.encode()).hexdigest()
SRP refactor: User data split from UserValidator, UserRepository, EmailService and PasswordHasher, each with one responsibility
💡 Tip: Click dropdown to switch between languages
user.py
import hashlib
class User:
"""Single responsibility: Represent user data"""
def __init__(self, username: str, email: str, password: str):
self.username = username
self.email = email
self.password = password
class UserValidator:
"""Single responsibility: Validate user data"""
def validate(self, user: User) -> bool:
if not user.username:
raise ValueError("Username is required")
if "@" not in user.email:
raise ValueError("Invalid email")
return True
class UserRepository:
"""Single responsibility: Handle database operations"""
def save(self, user: User):
print(f"Saving {user.username} to database...")
# Database save logic
class EmailService:
"""Single responsibility: Handle email notifications"""
def send_welcome_email(self, email: str):
print(f"Sending welcome email to {email}...")
# Email sending logic
class PasswordHasher:
"""Single responsibility: Handle password security"""
def hash(self, password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
# Usage
user = User("john_doe", "[email protected]", "secret123")
validator = UserValidator()
hasher = PasswordHasher()
if validator.validate(user):
user.password = hasher.hash(user.password)
repository = UserRepository()
repository.save(user)
email_service = EmailService()
email_service.send_welcome_email(user.email)
SRP-compliant class set: User, UserValidator, UserRepository, EmailService and PasswordHasher, each with one reason to change

Another common violation occurs when a single class handles HTTP requests, business logic, data formatting, and database operations.

💡 Tip: Click dropdown to switch between languages
bad_api_handler.py
class APIHandler:
def __init__(self):
self.db_connection = None # Database connection
def handle_request(self, request_data: dict):
# Responsibility 1: Handle HTTP request — parse request
user_id = request_data.get("user_id")
if user_id is None:
return {"error": "user_id is required"}
# Responsibility 2: Business logic — fetch user from database
user = self.fetch_user(user_id)
if not user:
return {"error": "User not found"}
# Responsibility 3: Format response — format JSON response
return {
"status": "success",
"data": {
"id": user["id"],
"name": user["name"],
"email": user["email"]
}
}
def fetch_user(self, user_id: int):
"""Responsibility 4: Database operations"""
# Database query logic (returns None if user not found)
if user_id <= 0:
return None
return {"id": user_id, "name": "John", "email": "[email protected]"}
💡 Tip: Click dropdown to switch between languages
api_handler.py
class UserService:
"""Single responsibility: Business logic"""
def __init__(self, repository):
self.repository = repository
def get_user(self, user_id: int):
user = self.repository.find_by_id(user_id)
if not user:
raise ValueError("User not found")
return user
class UserRepository:
"""Single responsibility: Data access"""
def find_by_id(self, user_id: int):
# Database query logic (returns None if user not found)
if user_id <= 0:
return None
return {"id": user_id, "name": "John", "email": "[email protected]"}
class ResponseFormatter:
"""Single responsibility: Response formatting"""
def format_success(self, data: dict):
return {
"status": "success",
"data": data
}
def format_error(self, error: str):
return {
"status": "error",
"message": error
}
class APIHandler:
"""Single responsibility: HTTP request handling"""
def __init__(self):
self.user_service = UserService(UserRepository())
self.formatter = ResponseFormatter()
def handle_request(self, request_data: dict):
try:
user_id = request_data.get("user_id")
if user_id is None:
return self.formatter.format_error("user_id is required")
user = self.user_service.get_user(user_id)
return self.formatter.format_success(user)
except ValueError as e:
return self.formatter.format_error(str(e))
SRP fix for an API handler: APIHandler delegating to UserService and ResponseFormatter, with UserService using UserRepository

A report system that processes data, generates reports, handles file operations, and sends email notifications violates SRP.

SRP violation: a single Report class that processes data, generates the report, saves it to file and sends email
💡 Tip: Click dropdown to switch between languages
bad_report.py
class Report:
def __init__(self, data):
self.data = data
def process_data(self):
"""Responsibility 1: Data processing"""
# Process and clean data
return [item for item in self.data if item.get("active")]
def generate_report(self):
"""Responsibility 2: Report generation"""
processed = self.process_data()
return f"Report: {len(processed)} items"
def save_to_file(self, filename: str):
"""Responsibility 3: File operations"""
report = self.generate_report()
with open(filename, 'w') as f:
f.write(report)
def send_email(self, recipient: str):
"""Responsibility 4: Email notification"""
report = self.generate_report()
print(f"Sending report to {recipient}: {report}")
SRP fix for reports: DataProcessor feeding ReportGenerator, which uses FileManager to save and EmailNotifier to send
💡 Tip: Click dropdown to switch between languages
report.py
class DataProcessor:
"""Single responsibility: Process and clean data"""
def __init__(self, data):
self.data = data
def process(self):
return [item for item in self.data if item.get("active")]
class ReportGenerator:
"""Single responsibility: Generate report content"""
def __init__(self, processed_data):
self.processed_data = processed_data
def generate(self) -> str:
return f"Report: {len(self.processed_data)} items"
class FileManager:
"""Single responsibility: Handle file operations"""
def save(self, content: str, filename: str):
with open(filename, 'w') as f:
f.write(content)
class EmailNotifier:
"""Single responsibility: Handle email notifications"""
def send(self, content: str, recipient: str):
print(f"Sending report to {recipient}: {content}")
# Usage
data = [{"id": 1, "active": True}, {"id": 2, "active": False}]
processor = DataProcessor(data)
processed = processor.process()
generator = ReportGenerator(processed)
report_content = generator.generate()
file_manager = FileManager()
file_manager.save(report_content, "report.txt")
email_notifier = EmailNotifier()
email_notifier.send(report_content, "[email protected]")

Remember: SRP is about cohesion - keeping related things together and unrelated things apart.

Quick recall

Make this lesson stick

Answer from memory, then check yourself. No typing or sign-in needed.

Question 1 of 3

What is the Single Responsibility Principle (SRP)?

💡 Time to Practice!

Now that you understand the concepts, put them into practice with our interactive playground. Build UML diagrams, write code, and get AI-powered feedback.

Browse All Problems