Suggest an editImprove this articleRefine the answer for “What is Single Responsibility Principle (SRP)?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Single Responsibility Principle (SRP)** - a class should have only one reason to change. Split by change driver: `User` holds data, `UserRepository` handles DB, `EmailService` sends emails. A DB outage shouldn't force you to edit the same file as an email bug. **Key:** one reason to change, not one method per class.Shown above the full answer for quick recall.Answer (EN)Image**Single Responsibility Principle (SRP)** - a class should have only one reason to change, meaning each class handles exactly one concern. ## Theory ### TL;DR - Think of a restaurant kitchen: the chef cooks, the waiter serves, the dishwasher cleans. Mix those roles and a change in one breaks the others. - SRP splits code by *why* it changes, not by how many methods it has. - A class changes for two unrelated reasons? Split it into two classes. - Decision rule: if a DB outage and an email outage both force you to open the same file, that class violates SRP. ### Quick example ```javascript // BAD: User changes for DB updates AND email outages - two reasons class User { constructor(name, email) { this.name = name; this.email = email; } save() { db.insert(this); } // DB reason sendEmail() { mailer.send(this); } // Email reason } // GOOD: each class changes for exactly one reason class User { constructor(name, email) { this.name = name; this.email = email; } } class UserRepository { save(user) { db.insert(user); } } class EmailService { send(user) { mailer.send(user.email); } } ``` After the split, a broken email service doesn't touch `UserRepository`. Tests stay isolated. That's the whole point. ### Key difference SRP is about *why* a class changes, not how many methods it has. A `User` class with 10 getters for name, email, and address follows SRP perfectly - it changes only when user data rules change. Adding `save()` breaks that. Now a DB schema change forces you to open a user-data file, which has nothing to do with data structure. ### When to use - A class grows past 50 lines and keeps expanding. Extract by change reason, not by line count. - Two separate teams (API team, reporting team) both edit the same class. Split at that boundary. - Your test suite for one class covers unrelated behaviors. Each test group points to a separate class. - Someone on the team says "this class does everything." Check git blame, find the top two change reasons, split there. ### How it works No runtime magic. SRP is a design rule enforced during code review and refactoring. That said, splitting large classes has a measurable side effect: V8's inline cache works better on smaller, focused allocation units. A "god class" with mixed responsibilities can cause megamorphic inline cache misses in hot paths, which slows execution. So splitting isn't only about readability. ### Common mistakes **Mistake: "few methods means SRP."** ```javascript class User { getName() { return this.name; } getEmail() { return this.email; } save() { db.insert(this); } // breaks SRP - second reason to change } ``` Three methods, two reasons to change. Extract `save()` to `UserRepository`. Problem gone. **Mistake: god object for convenience.** From what I've seen, this almost always starts with "I'll clean this up later." Hidden dependencies between concerns grow until a DB migration breaks an email test and no one knows why. Check your git log - if one class appears in unrelated commits, split it. **Mistake: TypeScript interface with mixed concerns.** ```typescript // Looks clean, violates SRP interface UserService extends UserRepository, Emailer {} ``` TypeScript's structural typing lets you do this. Lint won't catch it. But when your DB changes, you're editing an interface that also describes email behavior. **Mistake: over-splitting into nano-classes.** SRP doesn't mean one method per class. If two methods always change together for the same reason, they belong in the same class. Over-splitting adds indirection without benefit. ### Real-world usage - **Express.js:** thin route handlers delegate to `UserRepository` and `EmailService` separately. Controllers don't touch DB logic directly. - **React:** components render only. Data fetching goes into a custom hook or HOC. Analytics tracking goes into `useTrack`. Each piece changes independently. - **NestJS:** `@Controller`, `@Injectable` service, and repository are three separate classes by convention. The framework guides you toward SRP. - **Prisma:** Prisma client handles queries. Business logic lives in a separate service layer. Never mix them. ### Follow-up questions **Q:** How is SRP different from "one method per class"? **A:** SRP groups by change reason, not method count. A class with 10 getters for user data follows SRP. Adding `save()` breaks it because DB changes are now a second reason to edit the same file. **Q:** How would you refactor a legacy class that violates SRP without rewriting everything? **A:** Strangler pattern. Extract one responsibility at a time behind a feature flag. Prioritize by change frequency: grep git log for the class name, find which types of changes appear most, start there. **Q:** What are the tradeoffs of SRP in a microservices architecture? **A:** More classes, more files, more dependencies to wire up. But each service scales and deploys independently. The measure: do unrelated changes collide in the same file? If they don't, SRP is working. **Q:** How do you test SRP compliance? **A:** One test suite per class. If your tests for `User` mock a DB client and also mock an email client, the class does too much. ## Examples ### Basic: splitting a User class ```javascript // Three responsibilities in one class: data, persistence, email class BadUser { constructor(name, email) { this.name = name; this.email = email; } save() { db.insert(this); } // changes when DB changes sendWelcome() { mailer.send(this.email); } // changes when email logic changes } // After SRP split class User { constructor(name, email) { this.name = name; this.email = email; } } class UserRepository { save(user) { db.insert(user); } // only changes for DB reasons } class WelcomeEmailService { send(user) { mailer.send(user.email); } // only changes for email reasons } const user = new User('Alice', 'alice@example.com'); new UserRepository().save(user); // "Saved to DB" new WelcomeEmailService().send(user); // "Email sent" ``` A DB outage now changes only `UserRepository`. Email tests keep passing. That's the practical gain from splitting here. ### Intermediate: Express route handler (production pattern) ```javascript // BAD: one handler mixes validation, DB, and email app.post('/users', (req, res) => { const user = req.body; // no validation db.users.insert(user); // DB logic email.sendWelcome(user.email); // Email logic res.json({ success: true }); }); // GOOD: handler delegates to focused classes class UserValidator { static validate(data) { if (!data.email) throw new Error('Email required'); return data; } } class UserRepository { async save(data) { return db.users.create(data); } } class WelcomeEmailService { async send(email) { /* Nodemailer logic */ } } app.post('/users', async (req, res) => { const validated = UserValidator.validate(req.body); // throws on bad input await new UserRepository().save(validated); await new WelcomeEmailService().send(validated.email); res.json({ success: true }); }); ``` When your DB switches from Postgres to MongoDB, only `UserRepository` changes. Validation and email tests keep passing. Each class has exactly one reason to open it.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.