Single Responsibility Principle
A class should have only one reason to change.
Single Responsibility Principle (SRP): A Complete Guide with Examples in 11 Programming Languages
The Single Responsibility Principle states that a class should have only one reason to change — meaning it should have only one job, one responsibility, one purpose. When a class serves multiple unrelated concerns at once, a change to one concern risks breaking the others. SRP is the discipline of drawing clean boundaries between responsibilities so that each piece of code has a single, well-defined purpose.
In this comprehensive guide, we'll explore SRP with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common violations, and clear guidance on when and how to apply this foundational principle.
Table of Contents
- What is the Single Responsibility Principle?
- Why Follow SRP?
- SRP Comparison: Violation vs. Compliance
- SRP Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Single Responsibility Principle?
SRP is the first of the five SOLID principles, introduced by Robert C. Martin. Its deceptively simple definition — a class should have only one reason to change — conceals a profound design insight: that mixing concerns leads to fragile, hard-to-test, and hard-to-maintain code.
Consider a User class that handles user data, validates input, sends confirmation emails, and writes to a database. This class has four reasons to change: whenever the data model evolves, whenever validation rules update, whenever the email provider changes, and whenever the database schema shifts. Each of those changes puts the entire class at risk. A developer fixing an email bug must open the same file that houses the critical database logic.
SRP demands that you separate these concerns. A User class holds user data. A UserValidator validates it. A UserMailer sends emails. A UserRepository persists it. Each class has one reason to change — and changes to one class never accidentally affect the others.
The key question to ask of any class: "What does this class do?" If the answer contains the word and — "it validates and sends emails and saves to the database" — it has more than one responsibility.
Why Follow SRP?
SRP provides several critical benefits:
- Easier to Understand: A class with one responsibility is shorter, focused, and self-describing. A developer can understand its entire purpose in seconds.
- Safer to Change: A bug fix or feature in one responsibility cannot accidentally break another. The blast radius of any change is contained.
- Easier to Test: A class with one responsibility has fewer dependencies, fewer edge cases, and a smaller surface area. Unit tests are precise and fast.
- Reusability: A focused
EmailSenderclass can be reused acrossUserMailer,OrderNotifier, andPasswordResetMailer— a class that mixes email into user logic cannot. - Better Collaboration: In a team, separate classes mean separate files — multiple developers can work on related features without merge conflicts.
- Open/Closed Compliance: Classes with one responsibility are far easier to extend without modification. A class that does five things fights you at every extension point.
SRP Comparison: Violation vs. Compliance
| Dimension | SRP Violation | SRP Compliant |
|---|---|---|
| Class size | Large, growing | Small, stable |
| Reasons to change | Multiple (data, rules, I/O, format) | One |
| Test setup | Complex — many dependencies to mock | Simple — one concern to isolate |
| Reuse | Difficult — concerns are tangled | Easy — each class is independently usable |
| Team collaboration | Merge conflicts — many features in one file | Clean — one feature per file |
| Bug blast radius | A bug fix risks breaking unrelated behavior | A bug fix is contained to one concern |
| Naming | Hard to name — "UserManagerServiceHelper" | Easy — "UserValidator", "UserRepository" |
SRP vs. Other Principles:
- SRP vs. DRY (Don't Repeat Yourself): SRP is about separation of concerns, not elimination of duplication. Two classes can have similar code without violating SRP if their reasons to change are genuinely independent.
- SRP vs. High Cohesion: SRP and cohesion are deeply related. A class with high cohesion has methods and data that belong together for one purpose — which is exactly what SRP demands. Low cohesion is a symptom of SRP violation.
- SRP vs. God Object Anti-Pattern: The God Object — a class that knows and does everything — is the extreme SRP violation. SRP is the antidote.
SRP Explained
What Is a "Responsibility"?
"Responsibility" means a reason to change. A class has one responsibility if there is only one type of actor or one type of requirement change that would cause you to modify it.
Robert Martin defines it in terms of actors: a class should serve one actor. If the HR department and the Accounting department both depend on the same Employee class for different reasons, the class serves two actors — and changes requested by one may inadvertently break the other.
Three practical signals that a class violates SRP:
- The class name contains "And", "Or", or "Manager":
UserParserAndSender,DataProcessorOrFormatter,ReportManagerall hint at multiple concerns. - The class imports from unrelated domains: A class that imports both a database driver and an email library in the same file is almost certainly doing too much.
- Testing requires mocking multiple unrelated systems: If you must mock both a database and an SMTP server to test a single class, the class touches two concerns.
How to Decompose Responsibilities
When decomposing a class that violates SRP, follow these steps:
- List what the class currently does — write out every distinct operation.
- Group by "why would this change" — cluster operations by the type of requirement that would force a change.
- Extract each cluster into its own class — name each class after its single responsibility.
- Wire them together at the composition root — use dependency injection or direct instantiation at the top level to assemble the components.
Layers of Responsibility
SRP applies at every level:
- Method level: A method should do one thing.
- Class level: A class should have one reason to change.
- Module/package level: A module should serve one domain concern.
Class Diagram
Here's the UML class diagram showing a report generation system refactored to comply with SRP:
Beginner-Friendly Example
Let's start with the most intuitive SRP example: a user registration system. We'll first show the violation — a single User class doing everything — then refactor it into focused, single-responsibility classes.
# ── ❌ SRP VIOLATION ───────────────────────────────────────────────
# This class has four responsibilities: data storage, validation,
# persistence, and email notification. Four reasons to change.
class User:
def __init__(self, name: str, email: str, password: str):
self.name = name
self.email = email
self.password = password
def validate(self) -> bool:
if "@" not in self.email:
return False
if len(self.password) < 8:
return False
return True
def save_to_database(self) -> None:
# Connects to DB and saves — database concern mixed into User
print(f" [DB] INSERT INTO users (name, email) VALUES ('{self.name}', '{self.email}')")
def send_welcome_email(self) -> None:
# Sends email — email concern mixed into User
print(f" [EMAIL] Sending welcome email to {self.email}")
# ── ✅ SRP COMPLIANT ───────────────────────────────────────────────
from dataclasses import dataclass
# Responsibility 1: Data container — changes only when the User model changes
@dataclass
class User:
name: str
email: str
password: str
# Responsibility 2: Validation — changes only when validation rules change
class UserValidator:
def validate(self, user: User) -> bool:
if "@" not in user.email or "." not in user.email:
print(f" ❌ Invalid email: {user.email}")
return False
if len(user.password) < 8:
print(f" ❌ Password too short (minimum 8 characters)")
return False
return True
# Responsibility 3: Persistence — changes only when the database changes
class UserRepository:
def save(self, user: User) -> None:
print(f" [DB] INSERT INTO users (name, email) VALUES ('{user.name}', '{user.email}')")
def find_by_email(self, email: str) -> User | None:
print(f" [DB] SELECT * FROM users WHERE email = '{email}'")
return None # Simplified for demonstration
# Responsibility 4: Notification — changes only when email logic changes
class UserMailer:
def send_welcome(self, user: User) -> None:
print(f" [EMAIL] → {user.email}: Welcome to the platform, {user.name}!")
# ── Composition Root ───────────────────────────────────────────────
# Wire the components together — each class used independently
class UserRegistrationService:
def __init__(
self,
validator: UserValidator,
repository: UserRepository,
mailer: UserMailer,
) -> None:
self._validator = validator
self._repository = repository
self._mailer = mailer
def register(self, name: str, email: str, password: str) -> bool:
user = User(name=name, email=email, password=password)
if not self._validator.validate(user):
return False
self._repository.save(user)
self._mailer.send_welcome(user)
print(f" ✅ User '{name}' registered successfully.")
return True
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
service = UserRegistrationService(
validator=UserValidator(),
repository=UserRepository(),
mailer=UserMailer(),
)
print("=== User Registration ===\n")
service.register("Alice", "alice@example.com", "securepassword")
print()
service.register("Bob", "not-an-email", "short")
Production-Ready Example
Now let's build a report generation pipeline — a real-world scenario where SRP violations are common. A report system typically needs to fetch data, format it in multiple ways, and deliver it via email or printer. Without SRP, these concerns collapse into one bloated class. With SRP, each step is an independent, replaceable component.
🐍 Python
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date
from typing import Protocol
# ── Domain Models ──────────────────────────────────────────────────
@dataclass
class ReportRow:
label: str
value: float
region: str
@dataclass
class Report:
title: str
rows: list[ReportRow]
generated_at: date = field(default_factory=date.today)
# ── Responsibility 1: Data Fetching ────────────────────────────────
class SalesRepository:
"""Fetches sales data from the data source.
Changes only when the database schema or query logic changes."""
def get_quarterly_report(self, quarter: int, year: int) -> Report:
# In production: query database
return Report(
title=f"Q{quarter} {year} Sales Report",
rows=[
ReportRow("North", 45_200.00, "north"),
ReportRow("South", 38_750.50, "south"),
ReportRow("East", 52_100.75, "east"),
ReportRow("West", 29_900.00, "west"),
]
)
# ── Responsibility 2: Formatting ────────────────────────────────────
class HtmlReportFormatter:
"""Formats report data as HTML.
Changes only when the HTML template or layout changes."""
def format(self, report: Report) -> str:
rows_html = "\n".join(
f" <tr><td>{r.label}</td><td>${r.value:,.2f}</td></tr>"
for r in report.rows
)
total = sum(r.value for r in report.rows)
return (
f"<html><body>\n"
f" <h1>{report.title}</h1>\n"
f" <p>Generated: {report.generated_at}</p>\n"
f" <table>\n{rows_html}\n"
f" <tr><td><strong>Total</strong></td><td><strong>${total:,.2f}</strong></td></tr>\n"
f" </table>\n"
f"</body></html>"
)
class CsvReportFormatter:
"""Formats report data as CSV.
Changes only when the CSV structure or encoding changes."""
def format(self, report: Report) -> str:
lines = ["Label,Value,Region", f"# {report.title} — {report.generated_at}"]
for row in report.rows:
lines.append(f"{row.label},{row.value:.2f},{row.region}")
return "\n".join(lines)
# ── Responsibility 3: Delivery ──────────────────────────────────────
class EmailReportDelivery:
"""Sends formatted reports via email.
Changes only when email provider, address format, or SMTP config changes."""
def __init__(self, smtp_host: str) -> None:
self._smtp_host = smtp_host
def deliver(self, to: str, subject: str, content: str) -> None:
print(f" [SMTP:{self._smtp_host}] → {to}")
print(f" Subject: {subject}")
print(f" Body preview: {content[:80]}…")
class PrinterReportDelivery:
"""Sends formatted reports to a printer.
Changes only when printer drivers or page layout config changes."""
def __init__(self, printer_name: str) -> None:
self._printer_name = printer_name
def deliver(self, content: str) -> None:
print(f" [PRINTER:{self._printer_name}] Spooling {len(content)} bytes…")
print(f" Preview: {content[:60]}…")
# ── Responsibility 4: Orchestration ────────────────────────────────
class ReportService:
"""Orchestrates the pipeline: fetch → format → deliver.
Changes only when the overall pipeline logic changes."""
def __init__(
self,
repository: SalesRepository,
html_formatter: HtmlReportFormatter,
csv_formatter: CsvReportFormatter,
email_delivery: EmailReportDelivery,
printer_delivery: PrinterReportDelivery,
) -> None:
self._repo = repository
self._html = html_formatter
self._csv = csv_formatter
self._email = email_delivery
self._printer = printer_delivery
def email_html_report(self, quarter: int, year: int, to: str) -> None:
report = self._repo.get_quarterly_report(quarter, year)
content = self._html.format(report)
self._email.deliver(to, report.title, content)
print(f" ✅ HTML report emailed to {to}")
def print_csv_report(self, quarter: int, year: int) -> None:
report = self._repo.get_quarterly_report(quarter, year)
content = self._csv.format(report)
self._printer.deliver(content)
print(f" ✅ CSV report sent to printer")
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
service = ReportService(
repository=SalesRepository(),
html_formatter=HtmlReportFormatter(),
csv_formatter=CsvReportFormatter(),
email_delivery=EmailReportDelivery(smtp_host="smtp.company.com"),
printer_delivery=PrinterReportDelivery(printer_name="Office-HP-4520"),
)
print("=== Report Generation Pipeline ===\n")
service.email_html_report(quarter=2, year=2025, to="cfo@company.com")
print()
service.print_csv_report(quarter=2, year=2025)
Real-World Use Cases
SRP appears everywhere in well-designed systems. Here are the most common real-world applications:
Authentication Systems: Separate classes for CredentialValidator (validates username/password format), PasswordHasher (hashes and compares passwords), SessionManager (creates and invalidates sessions), and AuthLogger (logs login attempts). Each has one reason to change: a new hashing algorithm only touches PasswordHasher; a new session storage backend only touches SessionManager.
E-Commerce Order Processing: OrderValidator (checks stock, address, payment method), PricingEngine (applies discounts, taxes, coupons), InventoryService (decrements stock), PaymentProcessor (charges the card), ShipmentService (creates shipment), OrderNotifier (sends confirmation email). Six classes, six reasons to change. Swap the payment gateway? Only PaymentProcessor changes.
File Import Pipelines: FileParser (reads CSV/JSON/XML), DataNormalizer (standardizes formats), DataValidator (enforces business rules), DataImporter (writes to DB), ImportReporter (logs summary). A new input format only touches FileParser; a new validation rule only touches DataValidator.
REST API Layers: RequestValidator (validates input DTOs), ServiceLayer (orchestrates business logic), Repository (data access), ResponseMapper (maps domain objects to DTOs), ErrorHandler (standardizes error responses). This is the essence of clean layered architecture — each layer has one concern.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Mixing Business Logic with I/O in the Same Class
# BAD: The Order class knows about files, databases, AND business rules
class Order:
def __init__(self, items):
self.items = items
def calculate_total(self):
return sum(i.price for i in self.items)
def save_to_csv(self, filepath):
# Disk I/O mixed into the domain object
with open(filepath, 'w') as f:
for item in self.items:
f.write(f"{item.name},{item.price}\n")
def save_to_db(self, conn):
# DB I/O also mixed in — three reasons to change
cursor = conn.cursor()
cursor.execute("INSERT INTO orders ...", (self.calculate_total(),))
# GOOD: Separate classes for each concern
class Order:
def __init__(self, items):
self.items = items
def calculate_total(self):
return sum(i.price for i in self.items)
class OrderCsvExporter:
def export(self, order: Order, filepath: str) -> None:
with open(filepath, 'w') as f:
for item in order.items:
f.write(f"{item.name},{item.price}\n")
class OrderRepository:
def save(self, order: Order, conn) -> None:
cursor = conn.cursor()
cursor.execute("INSERT INTO orders ...", (order.calculate_total(),))
❌ Mistake #2: Fat __init__.py Modules That Act as God Objects
# BAD: utils.py doing everything — a module-level SRP violation
# utils.py
def send_email(to, subject, body): ...
def hash_password(password): ...
def parse_csv(filepath): ...
def generate_pdf(data): ...
def resize_image(path, width): ...
# GOOD: Split into focused modules
# email/sender.py — EmailSender class
# auth/hasher.py — PasswordHasher class
# files/csv.py — CsvParser class
# files/pdf.py — PdfGenerator class
# media/images.py — ImageResizer class
Frequently Asked Questions
Q1: How do I know if a class has too many responsibilities?
The clearest signal is the "reason to change" test: list every type of requirement change that would force you to open and edit the class. If your list has more than one distinct category of change (e.g., "change if the database schema changes" AND "change if the email provider changes"), the class has more than one responsibility.
A practical shortcut is the naming test: if you struggle to give the class a simple, precise name without using "And", "Or", "Manager", or "Helper", it's doing too much. UserRegistrationAndEmailNotificationAndDatabasePersistenceService is a name that reveals a violation.
Q2: Does SRP mean every class should have only one method?
No. SRP means one reason to change, not one method. A UserValidator class might have validateEmail(), validatePassword(), and validateUsername() — three methods, but all three belong to the same responsibility (validation). They share the same reason to change: a new validation rule. That's perfectly SRP-compliant.
Q3: Can SRP lead to too many small classes?
Yes — over-application of SRP can result in an explosion of tiny classes that make the codebase harder to navigate. The goal is not to maximize the number of classes but to draw boundaries where they provide real value: where reasons to change are genuinely independent, where reuse is likely, or where testability demands isolation. Two methods that always change together and are never used independently don't need to be in separate classes, even if they're logically distinct.
Q4: How does SRP relate to the "God Object" anti-pattern?
The God Object — a class that holds all application state and orchestrates all behavior — is the maximum violation of SRP. It has every reason in the system to change. SRP directly prevents the God Object from forming by ensuring each class is focused on a single concern from the start.
Q5: Should data classes (DTOs, records, structs) follow SRP?
Yes, in the sense that a data class should represent one domain concept. A UserDTO should not double as an OrderDTO with conditionally populated fields. However, data classes typically have no behavior (methods), so they rarely accumulate multiple reasons to change. The SRP focus for data classes is ensuring each class represents exactly one concept — not mixing unrelated data in one container.
Q6: How do I apply SRP in functional programming (no classes)?
In functional programming, SRP applies at the function and module level. A function should do one thing. A module should export functions for one domain area. The equivalent of mixing concerns in a class is writing a function that reads from a database, validates, transforms, and sends an email all in one function. SRP in FP means writing small, pure, composable functions and organizing them into focused modules.
Q7: Is SRP the same as "separation of concerns"?
SRP and Separation of Concerns (SoC) are closely related but not identical. SoC is a general design principle stating that a system should be divided into parts, each addressing a separate concern. SRP is the class-level application of SoC: a class should address only one concern. SoC is the broader principle; SRP is the specific rule that applies it at the class level in object-oriented design.
Q8: How do I refactor a legacy class that violates SRP?
Refactor incrementally: don't try to extract all responsibilities at once. Start with the most volatile responsibility — the one that changes most often. Extract it into a new, focused class and inject it via the constructor. Test both classes. Then extract the next responsibility. Repeat until the original class has one clear purpose. This approach is safer than a big-bang refactor and can be done alongside regular feature work.
Key Takeaways
🎯 Core Concept: The Single Responsibility Principle states that a class should have only one reason to change. "Responsibility" means a reason to change — not the number of methods, not the number of lines, but the number of distinct concerns that could independently force a modification. A class with one responsibility is focused, predictable, and safe to change.
🔑 Key Benefits:
- Containment: A change to one concern cannot accidentally break another — the blast radius of every change is bounded
- Testability: Classes with one responsibility have fewer dependencies, simpler setup, and more precise tests
- Reusability: A focused
EmailSendercan be used anywhere; a mixedUserWithEmailAndDatabaseclass is tied to its context - Naming: Classes with one responsibility are easy to name precisely — struggling to name a class is a signal of SRP violation
- Team scalability: Separate responsibilities mean separate files, reducing merge conflicts in growing teams
🌐 Language-Specific Highlights:
- Python: Use
@dataclassfor pure data models; keep__init__.pythin; split largeutils.pyfiles into focused modules by domain - TypeScript: Use
interfacefor data shapes and separate classes for each behavior layer; avoid combining HTTP, business, and DB logic in one class - Java: Keep JPA entities free of business logic; use
recordfor immutable data models; inject services via constructor, not singletons - JavaScript: Use custom hooks for data fetching and keep React components purely for rendering; separate transformation logic into pure functions
- C#: Controller methods should only parse input and delegate — all logic belongs in injected service/repository classes
- PHP: Use
readonlyconstructor promotion for clean data classes; avoid functions that mix HTML rendering with DB queries - Go: HTTP handler functions should only parse and delegate; define an interface for each service dependency; one concern per package
- Rust: Separate I/O (file loading, network) from business logic; use dedicated structs for validation, transformation, and persistence
- Dart: Keep Widgets as pure render functions; move data fetching and business logic into
ChangeNotifier/Bloc/provider classes - Swift: Avoid Massive View Controller — extract networking into services, business logic into ViewModels, and keep Views as pure display
- Kotlin: Android ViewModels should orchestrate, not fetch; use repositories for data access and mappers for transformation
📋 When to Apply SRP:
- A class has multiple imports from unrelated domains (e.g., database AND email AND HTTP)
- A class is hard to name without using "And", "Or", "Manager", or "Helper"
- A unit test requires mocking more than one unrelated external system
- Multiple teams or features are changing the same class simultaneously
- A bug in one feature is affecting an unrelated feature in the same class
⚠️ When NOT to Over-Apply SRP:
- Two methods always change together — they belong together even if logically distinct
- Splitting creates classes so small they have no meaning without their sibling
- The added indirection obscures the overall flow without providing real isolation value
- You're refactoring stable, tested code without a concrete reason — "might need to change" is not a reason
🛠️ Best Practices Across Languages:
- Apply the "reason to change" test: Before creating or modifying a class, ask how many distinct forces could drive a change to it
- Name classes after their single purpose: A class name should be a precise description of its one responsibility
- Use constructor injection: Dependencies injected via the constructor make responsibilities explicit and make classes easy to test
- Separate data from behavior: Data models (DTOs, records, structs) should hold data only; behavior belongs in dedicated service/validator/repository classes
- Refactor incrementally: Extract the most volatile responsibility first; don't try to fix all SRP violations in one pass
- Compose at the entry point: Wire together focused classes at the composition root (main function, DI container, app startup) — not within the classes themselves
- Watch module-level SRP: SRP applies at the module/package level too — a
utilsfile that does everything is a module-level God Object
💡 Common Pitfalls:
- The "Manager" class: A class named
UserManageralmost always violates SRP — it manages everything from validation to persistence to notification - Entities with behavior: Domain entities (User, Order, Product) that contain validation, notification, and persistence logic in addition to data — the most common SRP violation in web applications
- Fat controllers: HTTP controllers that contain business logic and database access directly — controllers should route, not compute
- Mixed layers in tests: If a unit test for a business rule is also testing email delivery, the tested class violates SRP
- Premature aggregation: Combining two classes "to reduce file count" when their reasons to change are genuinely independent — short-term convenience that creates long-term coupling
The Single Responsibility Principle is the foundation of maintainable object-oriented design. A codebase where every class has one clear purpose is a codebase where every change is safe, every test is fast, and every developer can find what they're looking for without fear of breaking something unrelated.
Want to dive deeper into SOLID principles? Check out our comprehensive guides on:
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP) Each guide includes examples in all 11 programming languages with language-specific best practices.