random content
In real-world projects, code often grows in a “just put everything in” manner. A service that initially had only 50 lines can suddenly turn into a 1,000-line monster with dozens of responsibilities after a few months. At that point, maintenance becomes a nightmare.
This is exactly when the Single Responsibility Principle (SRP) speaks up.
In short: A class or module should have only one reason to change.
If a class handles business logic, talks to the database, and sends notifications at the same time, it is violating SRP.
UserService in NestJSImagine you are writing a UserService. For convenience, you put everything into it:
@Injectable()
export class UserService {
constructor(private readonly db: DatabaseService) {}
async createUser(data: any) {
if (!data.email.includes('@')) {
throw new Error('Invalid email');
}
const user = await this.db.user.create({ data });
await this.sendWelcomeEmail(user.email);
return user;
}
private async sendWelcomeEmail(email: string) {
console.log(`Sending welcome email to ${email}`);
}
}
At first glance, this looks fine. But UserService is doing at least three different things:
Data validation
Database interaction
Sending emails
If tomorrow you want to change the email provider (from Gmail → SendGrid), you have to modify UserService itself.
If you want to reuse the validation logic elsewhere, you end up copying code.
This is a clear violation of SRP.
The solution: separate responsibilities into dedicated classes/services.
@Injectable()
export class UserValidator {
validate(data: any) {
if (!data.email.includes('@')) {
throw new Error('Invalid email');
}
}
}
@Injectable()
export class EmailService {
async sendWelcomeEmail(email: string) {
console.log(`Sending welcome email to ${email}`);
}
}
@Injectable()
export class UserService {
constructor(
private readonly db: DatabaseService,
private readonly validator: UserValidator,
private readonly emailService: EmailService,
) {}
async createUser(data: any) {
this.validator.validate(data);
const user = await this.db.user.create({ data });
await this.emailService.sendWelcomeEmail(user.email);
return user;
}
}Now:
UserValidator is responsible only for validation
EmailService handles email delivery
UserService focuses on its core business: creating users
If you need to change the email provider → only update EmailService.
Validation logic can be reused across different parts of the application.
Easier testing: you can write isolated unit tests for UserValidator or EmailService
Easier maintenance: changing one piece of logic doesn’t introduce bugs elsewhere
Reusability: validation can be reused by other APIs
Of course, don’t overdo the splitting. If your app is small, grouping two or three responsibilities in a single service is often fine. SRP truly shines when the project grows, the team gets larger, and the logic becomes more complex.
The Single Responsibility Principle helps keep your code clean, readable, and easy to extend. Each class should be able to answer one simple question:
“I exist to do one thing, and I do it really well.”