Rule
Classes should have single responsibility.
Classes handling multiple concerns violate
the Single Responsibility Principle.
Supported languages: JS, TS, PY, JAVA, C/C++,
C#, Swift/Objective C, Ruby. PHP, Kotlin,
Scala, Rust, Haskell, Groovy, Dart. Julia,
Elixit, Klojure, OCaml, DelphiIntroduction
Classes that do too much become bottlenecks. A class handling authentication, emails, and validation requires changes whenever any concern evolves, risking breaks in unrelated functionality. Testing requires mocking the entire class even when testing one aspect. The Single Responsibility Principle states a class should have only one reason to change.
Why it matters
Code maintainability: Classes with multiple responsibilities change more often because any concern's evolution affects the entire class.
Testing complexity: Testing multi-responsibility classes requires mocking all dependencies, even for testing one feature.
Reusability: You can't extract one responsibility without bringing all dependencies. Developers duplicate code rather than untangling multi-responsibility classes.
Team coordination: Multiple developers working on the same class for different features create frequent merge conflicts. Single-responsibility classes enable parallel development without conflicts.
Code examples
❌ Non-compliant:
class UserManager {
async createUser(userData) {
const user = await db.users.insert(userData);
await this.sendWelcomeEmail(user.email);
await this.logEvent('user_created', user.id);
await cache.set(`user:${user.id}`, user);
return user;
}
async sendWelcomeEmail(email) {
const template = this.loadEmailTemplate('welcome');
await emailService.send(email, template);
}
async logEvent(event, userId) {
await analytics.track(event, { userId, timestamp: Date.now() });
}
}
Why it's wrong: This class handles database operations, email sending, logging, and caching. Changes to email templates, logging formats, or cache strategy all require modifying this class. Testing user creation means mocking email services, analytics, and cache, making tests slow and brittle.
✅ Compliant:
class UserRepository {
async create(userData) {
return await db.users.insert(userData);
}
}
class EmailNotificationService {
async sendWelcomeEmail(email) {
const template = await this.templateLoader.load('welcome');
return await this.emailSender.send(email, template);
}
}
class UserEventLogger {
async logCreation(userId) {
return await this.analytics.track('user_created', {
userId,
timestamp: Date.now()
});
}
}
class UserService {
constructor(repository, emailService, eventLogger, cache) {
this.repository = repository;
this.emailService = emailService;
this.eventLogger = eventLogger;
this.cache = cache;
}
async createUser(userData) {
const user = await this.repository.create(userData);
await Promise.all([
this.emailService.sendWelcomeEmail(user.email),
this.eventLogger.logCreation(user.id),
this.cache.set(`user:${user.id}`, user)
]);
return user;
}
}
Why this matters: Each class has one clear responsibility: data persistence, email sending, event logging, or orchestration. Changes to email templates only affect EmailNotificationService. Testing user creation can use simple stubs for dependencies. Classes can be reused independently across different features.
Conclusion
The Single Responsibility Principle isn't about making classes as small as possible, it's about ensuring each class has one clear reason to change. When a class starts handling multiple concerns, refactor by extracting each responsibility into its own class with a focused interface. This makes code easier to test, maintain, and evolve without cascading changes across unrelated functionality.
.avif)
