Single Responsibility Principle
A class should have only one reason to change — keep each unit focused on a single job so unrelated concerns don't accidentally break each other.
★★★★★5/5Inside a codebase — classes, modules, files
How it works
The Single Responsibility Principle (SRP) is the 'S' in SOLID, articulated by Robert C. Martin in 1999. It states that a module or class should have one, and only one, reason to change. 'Reason to change' maps to 'a stakeholder or a concern' — if two different actors or concerns could require changes to the same class, that class has two responsibilities.
The canonical violation: an OrderService that places orders, sends emails, and generates PDF invoices. Three reasons to change — every feature request from the email team, the PDF team, or the order team touches the same file, causing merge conflicts and unintended regressions.
SRP is about cohesion: grouping things that change together, and separating things that change for different reasons. A class that does too much is a symptom of missing domain concepts — each missing concept is a new class waiting to be extracted.
Implementation
TypeScript · Go · Rust// ❌ Three reasons to change: order logic, email, and invoicing
class OrderService {
placeOrder(order: Order): void {
// ... validate and save order
this.db.save(order);
// Sending email has nothing to do with placing an order
const html = `<h1>Thanks for your order #${order.id}</h1>`;
this.mailer.send({ to: order.email, subject: "Order confirmed", html });
// PDF generation is a third concern
const pdf = this.pdfGen.render(order);
this.storage.upload(`invoices/${order.id}.pdf`, pdf);
}
}
// ✓ One reason to change per class
class OrderService {
constructor(
private repo: OrderRepository,
private emailer: OrderEmailer,
private invoicing: InvoiceService,
) {}
async placeOrder(order: Order): Promise<void> {
await this.repo.save(order); // one concern
await this.emailer.confirm(order); // delegates, doesn't own
await this.invoicing.generate(order); // delegates, doesn't own
}
}
class OrderEmailer {
async confirm(order: Order): Promise<void> { /* only email logic */ }
}
class InvoiceService {
async generate(order: Order): Promise<void> { /* only PDF logic */ }
}Why it matters
Classes with multiple responsibilities become magnets for unrelated change. Every new feature from every stakeholder lands in the same file. SRP keeps classes small, focused, and stable — a change to email sending cannot break order logic if they live in separate classes.
✓ When to use
- →Always — apply SRP to every class, module, and function
- →When a class is growing too large or has too many imports
- →When merge conflicts repeatedly happen in the same file
- →When testing a class requires setting up unrelated infrastructure
✗ When NOT to use
- →Over-splitting can create too many tiny classes that are hard to navigate — use judgment
- →In scripts and small utilities where a single file is genuinely simpler
Trade-offs
Changes to one concern cannot break other concerns
More classes means more files to navigate and more wiring
Classes are smaller and easier to understand and test
Finding which class owns a piece of logic requires knowledge of the system
Reduces merge conflicts in team settings
Premature decomposition before concerns are understood wastes time
In production
Fat model / skinny controller antipattern — ActiveRecord models accumulate too many responsibilities; service objects extract them
@Service, @Repository, @Controller annotations enforce SRP by role — each layer has one job
'Do one thing and do it well' — each Unix tool has one responsibility; pipes compose them
Industry adoption
Related principles
Open/Closed Principle
Software entities should be open for extension but closed for modification — add new behaviour by writing new code, not by changing existing code.
Interface Segregation Principle
Prefer many small, focused interfaces over one large general-purpose one — clients should not be forced to depend on methods they don't use.
Hexagonal Architecture
Place the domain at the centre and connect all I/O (HTTP, database, messaging) through explicit ports and adapters — so the core is framework-free and infinitely testable.
Clean Architecture
LiveOrganise code into concentric dependency rings so business logic never depends on frameworks, databases, or UI.