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.
December 4, 2025 (16d ago)
A Guide to the Single Responsibility Principle
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.
← Back to blog
A Guide to the Single Responsibility Principle
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) is simple but powerful: each class, module, or function should have only one reason to change. Applied well, SRP reduces bugs, makes testing straightforward, and keeps teams moving fast. This guide explains SRP, shows common violations, and walks through practical TypeScript refactors you can use today.
What Is the Single Responsibility Principle?

Think of SRP like choosing the right tool for the 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. This principle is a core part of SOLID design thinking1.
SRP at a Glance: Why It Matters
| Concept | What It Means in Practice |
|---|---|
| A single reason to change | A module should change only when its one business purpose changes. |
| High cohesion | Everything inside the module belongs together. |
| Low coupling | The module doesn’t rely on other modules’ internals. |
| Clear boundaries | It’s obvious where one responsibility ends and another begins. |
| Violation indicators | Importing from many domains (UI, DB, APIs) 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 Upfront
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 agility2.
Why SRP Is Your Secret Weapon for Maintainable Software
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.
Accelerating Debugging and 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.
The scope of a bug is limited to the scope of a single responsibility.
Streamlining 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 are essential for teams building scalable systems and shipping features safely. See examples in our client work at microestimates.com and fluidwave.com.
Enhancing Testability and Reusability
A single-purpose function is straightforward to unit test. By contrast, testing a multi-responsibility class often requires complex mocks and brittle test setups. Extracting services like EmailService makes reuse easy across the codebase and avoids unnecessary duplication.
How to Spot Common SRP Violations

SRP violations are often subtle and grow over time. Ask yourself: “How many different reasons could cause me to change this file?” If you can list more than one business or technical reason, 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. We 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 (e.g.,
useUserProfile). - Create presentational components (
UserProfileForm,UserDetailsDisplay) that only render UI.
Separating logic from presentation makes components easier to test and reuse and prevents UI churn when business rules change.
Refactoring Code to Follow the Single Responsibility Principle

Refactoring an SRP violation is intentional work: extract responsibilities into specialists and keep a thin orchestrator to coordinate them.
Evolving the Monolithic UserService
We can 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 original class is now an orchestrator
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.
useUserProfilehandles API calls, loading and error states, and form submission.UserProfileFormrenders inputs and delegates submit handling.
This separation improves testability and makes UI stable when business rules change. Presentational components can be documented with Storybook for isolated testing and reuse.
Applying SRP Beyond Just Classes
SRP is a design philosophy that 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.
How to Weave SRP into Your Team's Workflow
Adopting SRP requires guardrails, not just rules. Use linters, code review checklists, and team habits to make SRP the default.
Use Linters as Your First Line of Defence
Linter rules can flag likely SRP violations:
max-lines-per-functionto avoid monster functions.complexityto catch high cyclomatic complexity.max-paramsto 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:
- What is the one job of this module? If you say “and,” rethink it.
- How many different reasons would we have to change this file?
- Can any logic be pulled into a reusable hook or service?
- 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 (e.g., 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.
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.