December 4, 2025 (2mo ago) — last updated January 28, 2026 (24d ago)

Single Responsibility Principle (SRP) Guide

Learn the Single Responsibility Principle (SRP), why it matters, and how to apply it with practical TypeScript examples and refactor patterns.

← Back to blog
Cover Image for Single Responsibility Principle (SRP) Guide

The Single Responsibility Principle (SRP) says each class, module, or function should have one clear reason to change. This guide explains SRP, shows common violations, and provides practical TypeScript refactors you can apply today.

Single Responsibility Principle (SRP) Guide

Summary: A complete guide to the Single Responsibility Principle (SRP). Learn what it is, why it matters, and how to apply it with real-world TypeScript examples.

Introduction

The Single Responsibility Principle (SRP) says each class, module, or function should have one clear reason to change. Applied well, SRP reduces bugs, makes testing straightforward, and helps teams move faster. This guide explains SRP, shows common violations, and walks through practical TypeScript refactors you can apply today. SRP is a core idea in SOLID design thinking1.

What is the Single Responsibility Principle?

Think of SRP like choosing the right tool for a job. A Swiss Army knife is versatile but not ideal when you need a proper screwdriver. In software, a class that mixes unrelated tasks—database access, email delivery, and business rules—becomes fragile. SRP says group code by a single, cohesive responsibility so changes stay local and predictable.

SRP at a glance: why it matters

ConceptWhat it means in practice
A single reason to changeA module should change only when its one business purpose changes.
High cohesionEverything inside the module belongs together.
Low couplingThe module doesn’t rely on other modules’ internals.
Clear boundariesIt’s obvious where one responsibility ends and another begins.
Violation indicatorsImporting from many domains or mixing unrelated logic.

Following SRP makes code easier to read, test, and maintain.

The core idea: a single reason to change

“A single reason to change” doesn’t mean a class can only have one method. It means all methods should support one cohesive goal. For example, a User class that handles permissions, persistence, and report formatting has multiple reasons to change and should be split into focused components.

Common triggers that should live in different modules include:

  • Business rules for permissions
  • Database schema changes
  • Report formatting or export

Why SRP matters early

Adopting SRP early prevents architectural entropy. Small, focused classes are easier to reason about than monoliths that try to do everything. Clear responsibilities reduce cognitive load for new team members and speed up feature delivery. Modular design also enables independent deployment and scaling, which improves organizational agility and team performance2.

SRP benefits: maintainability, testing, and team velocity

SRP separates code that merely works from code that’s maintainable. When components have a single job, onboarding is faster, debugging is surgical, and refactors are lower risk. Smaller modules also make code review and ownership clearer, which reduces merge conflicts and increases velocity across teams.

Faster debugging and better fault isolation

With SRP, bugs are easier to find: payment issues live in PaymentService, not spread across a dozen files. Limiting a bug’s scope makes fixing it faster and reduces the chance a fix breaks something else.

“Once responsibilities are separated, the scope of a bug is limited to the scope of a single responsibility.”

Streamlined team collaboration

SRP enables parallel work with fewer conflicts:

  • Reduced merge conflicts when teams own separate services.
  • Clear ownership that builds deep domain expertise.
  • Smaller, focused code reviews that yield actionable feedback.

These practices support scalable systems and safer releases2.

Improved testability and reuse

A single-purpose function is straightforward to unit test. A multi-responsibility class often requires complex mocks and brittle setups. Extracting services like EmailService makes reuse simple and avoids duplication.

How to spot SRP violations

SRP violations are often subtle and grow over time. Ask: “How many distinct reasons could cause me to change this file?” If more than one business or technical reason exists, you likely have an SRP smell.

The overloaded service class

A common backend smell is an overloaded service class. A UserService that starts with registration can end up handling authentication, profile updates, notifications, and DB connections. That’s a clear SRP violation.

Anti-pattern example (TypeScript):

// ANTI-PATTERN: This class has too many responsibilities
class UserService {
  saveUserToDatabase(user: object) {
    console.log('Saving user to the database...', user);
  }

  handlePasswordHashing(password: string): string {
    console.log('Hashing the password...');
    return `hashed_${password}`;
  }

  sendWelcomeEmail(email: string) {
    console.log(`Sending a welcome email to ${email}...`);
  }

  registerUser(email: string, password: string) {
    const hashedPassword = this.handlePasswordHashing(password);
    const user = { email, password: hashedPassword };
    this.saveUserToDatabase(user);
    this.sendWelcomeEmail(email);
  }
}

This entanglement increases testing complexity and risk. Prefer focused services that do one job well.

The bloated React component

On the frontend, “god components” are the usual offenders: a UserProfile that fetches data, manages state, handles validation, and renders complex UI. That mixing of concerns creates fragility.

Refactor with two moves:

  • Extract logic into a custom hook, for example useUserProfile.
  • Create presentational components like UserProfileForm and UserDetailsDisplay that only render UI.

Separating logic from presentation makes components easier to test and reuse. Presentational components can be documented in Storybook for isolated testing.

Refactoring to follow SRP

Refactoring an SRP violation is intentional work: extract responsibilities into specialists and keep a thin orchestrator to coordinate them.

Evolving the monolithic UserService

Split the overloaded UserService into three services:

  • AuthService: password hashing and verification
  • UserRepository: database reads and writes
  • NotificationService: outbound communications

Refactored example (TypeScript):

// Responsibility 1: Authentication logic
class AuthService {
  hashPassword(password: string): string {
    console.log('Hashing password securely...');
    return `hashed_${password}`;
  }
}

// Responsibility 2: Data persistence
class UserRepository {
  saveUser(user: object) {
    console.log('Saving user to the database...', user);
  }
}

// Responsibility 3: Notifications
class NotificationService {
  sendWelcomeEmail(email: string) {
    console.log(`Sending welcome email via external service to ${email}...`);
  }
}

// The orchestrator now delegates responsibilities
class UserService {
  private authService: AuthService;
  private userRepository: UserRepository;
  private notificationService: NotificationService;

  constructor() {
    this.authService = new AuthService();
    this.userRepository = new UserRepository();
    this.notificationService = new NotificationService();
  }

  registerUser(email: string, password: string) {
    const hashedPassword = this.authService.hashPassword(password);
    const user = { email, password: hashedPassword };

    this.userRepository.saveUser(user);
    this.notificationService.sendWelcomeEmail(email);
  }
}

Now a change to password hashing touches only AuthService. Database changes touch only UserRepository. Notifications are isolated in NotificationService.

Deconstructing the bloated React component

Move data fetching and form handling into a hook like useUserProfile. Keep presentational components purely for rendering.

  • useUserProfile handles API calls, loading and error states, and form submission
  • UserProfileForm renders inputs and delegates submit handling

This separation improves testability and makes UI stable when business rules change.

SRP beyond classes

SRP applies to functions, components, and architecture. A function named getUserAndFormatReport is a red flag—split it into getUser and formatReport. In UI frameworks, components should do one job: a Button renders a button and handles clicks; a UserProfileCard displays user info.

At the architectural level, SRP maps well to microservices: authentication, inventory, and payments each own distinct capabilities, enabling independent updates and scaling2.

“A system built with SRP at every level—from function to microservice—is a system where complexity is managed, not just shuffled around.”

This idea also applies to team structure: giving a team clear ownership over a domain improves focus and speed.

Weaving SRP into your team workflow

Adopting SRP requires guardrails, not just rules. Use linters, code review checklists, and team habits to make SRP the default.

Use linters as a first line of defence

Linter rules can flag likely SRP violations:

  • max-lines-per-function to avoid monster functions
  • complexity to catch high cyclomatic complexity
  • max-params to spot functions coordinating too many things

These rules are nudges that encourage refactoring early.

Conduct code reviews with an SRP lens

Make SRP part of code review with these questions:

  1. What is the one job of this module? If you say “and,” rethink it
  2. How many different reasons would we have to change this file?
  3. Can any logic be pulled into a reusable hook or service?
  4. Is this component mixing data fetching, state management, and rendering?

Use these prompts to build a shared language for clean code and to keep SRP in regular practice.

Frequently asked questions about SRP

Isn’t SRP just about having one method per class?

No. SRP isn’t about method count. It’s about purpose. A UserRepository may have multiple methods—findById, createUser, updateEmail, deleteUser—and still respect SRP because all methods serve a single responsibility: managing user persistence.

Can you take SRP too far?

Yes. Over-abstraction leads to a class explosion. Aim for pragmatic boundaries: group things that change for the same business reason. If things change together, they probably belong together.

When should I refactor for SRP?

Refactor when a module has multiple unrelated reasons to change, when tests are hard to write, or when onboarding becomes slow. Small, continuous refactors are better than big, risky rewrites.


Quick Q&A (Concise answers)

Q: How do I know a module violates SRP?

A: Ask how many distinct business or technical reasons would cause the file to change. More than one indicates a violation.

Q: What’s the first refactor step for an overloaded class?

A: Extract responsibilities into focused services (for example, AuthService, UserRepository, NotificationService) and keep a thin orchestrator.

Q: How does SRP improve testing?

A: Single-purpose units are easier to mock and test in isolation, reducing brittle, high-maintenance test suites.


Three common questions developers ask

Q: Should a service have no dependencies?

A: No. Services will depend on other services, but those dependencies should be explicit and limited. Prefer composition over embedding unrelated logic.

Q: How granular should responsibilities be?

A: Aim for responsibilities that change for the same reason. If two concerns always change together, keep them together. If not, separate them.

Q: How do I balance SRP with delivery speed?

A: Use incremental refactors. Start with small, test-covered extracts and avoid large, risky rewrites.


1.
Robert C. Martin, “SOLID (object-oriented design),” Wikipedia, https://en.wikipedia.org/wiki/SOLID_(object-oriented_design).
2.
State of DevOps and organizational outcomes, Puppet and DORA research, https://puppet.com/resources/report/state-of-devops-report.
3.
Stack Overflow Developer Survey, TypeScript usage and trends, https://insights.stackoverflow.com/survey.
← Back to blog
🙋🏻‍♂️

AI writes code.
You make it last.

In the age of AI acceleration, clean code isn’t just good practice — it’s the difference between systems that scale and codebases that collapse under their own weight.