SOLID Principle · SRP

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?

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:

  1. Easier to Understand: A class with one responsibility is shorter, focused, and self-describing. A developer can understand its entire purpose in seconds.
  2. Safer to Change: A bug fix or feature in one responsibility cannot accidentally break another. The blast radius of any change is contained.
  3. 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.
  4. Reusability: A focused EmailSender class can be reused across UserMailer, OrderNotifier, and PasswordResetMailer — a class that mixes email into user logic cannot.
  5. Better Collaboration: In a team, separate classes mean separate files — multiple developers can work on related features without merge conflicts.
  6. 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

DimensionSRP ViolationSRP Compliant
Class sizeLarge, growingSmall, stable
Reasons to changeMultiple (data, rules, I/O, format)One
Test setupComplex — many dependencies to mockSimple — one concern to isolate
ReuseDifficult — concerns are tangledEasy — each class is independently usable
Team collaborationMerge conflicts — many features in one fileClean — one feature per file
Bug blast radiusA bug fix risks breaking unrelated behaviorA bug fix is contained to one concern
NamingHard 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:

  1. The class name contains "And", "Or", or "Manager": UserParserAndSender, DataProcessorOrFormatter, ReportManager all hint at multiple concerns.
  2. 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.
  3. 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:

  1. List what the class currently does — write out every distinct operation.
  2. Group by "why would this change" — cluster operations by the type of requirement that would force a change.
  3. Extract each cluster into its own class — name each class after its single responsibility.
  4. 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 EmailSender can be used anywhere; a mixed UserWithEmailAndDatabase class 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 @dataclass for pure data models; keep __init__.py thin; split large utils.py files into focused modules by domain
  • TypeScript: Use interface for 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 record for 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 readonly constructor 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:

  1. Apply the "reason to change" test: Before creating or modifying a class, ask how many distinct forces could drive a change to it
  2. Name classes after their single purpose: A class name should be a precise description of its one responsibility
  3. Use constructor injection: Dependencies injected via the constructor make responsibilities explicit and make classes easy to test
  4. Separate data from behavior: Data models (DTOs, records, structs) should hold data only; behavior belongs in dedicated service/validator/repository classes
  5. Refactor incrementally: Extract the most volatile responsibility first; don't try to fix all SRP violations in one pass
  6. Compose at the entry point: Wire together focused classes at the composition root (main function, DI container, app startup) — not within the classes themselves
  7. Watch module-level SRP: SRP applies at the module/package level too — a utils file that does everything is a module-level God Object

💡 Common Pitfalls:

  • The "Manager" class: A class named UserManager almost 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: