Skip to main content

The single responsibility principle — who "a reason to change" refers to

SRP's definition is short, memorable, and unusable as it stands. When you are told "a class should have one reason to change," nothing settles what counts as one reason.

Looking at the same class, one person says "this is all order processing, so it is one responsibility," and another says "there are three responsibilities here: calculating, saving, and notifying." Neither contradicts the definition. The definition does not fix the granularity.

This chapter confirms in the original text how Martin himself answered this weakness, and then covers what you lose when you split too far.

What the original said — this principle is about people

The most widely quoted formulation is this one.

each software module should have one and only one reason to change.

The source is an article Martin wrote in 20141. In the same article, he restates it this way.

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Up to here the word "reason" is left as is. Midway through the article, however, Martin makes explicit what "reason" refers to.

This principle is about people.

He then counts responsibilities by where the request for change comes from.

when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function

This is the turning point of the definition. It declares that "reason" was never a property inside the code but the people outside it. When counting responsibilities, what you look at is not the contents of the class but how many kinds of parties come to it asking for changes.

The example Martin gives is an Employee class with three methods: payroll calculation, hours reporting, and saving. What drives changes to each are the officers responsible for finance, operations, and technology — three separate people. And he writes:

we don't want to get the COO fired because we made a change requested by the CTO.

The phrasing is somewhat exaggerated, but the cost SRP is trying to reduce shows up here. It is the danger of requests made by different people for different purposes landing in the same code.

The formulation that calls these "people or groups who request changes" actors is known as the one Martin presented in his 2017 book Clean Architecture. This series has not examined a copy of that book, however, so the attribution of the word "actor" is treated as a secondary source. Given that "This principle is about people." appears in the 2014 article, the idea itself can be traced to a primary source that predates the book, so what follows tracks the article's wording.

Why follow it — stopping collateral changes

What SRP reduces is not line count or complexity. It is the cost of unintended collateral changes.

Take a library's staff records as an example. The following class bundles together pay calculation, work-hour totaling, and record saving.

❌ Bad: three kinds of requesters land in the same class
class StaffMember {
constructor(
private readonly name: string,
private readonly hourlyWage: number,
private readonly workedHours: number[],
) {}

// Total-hours calculation shared by two methods
private totalHours(): number {
return this.workedHours.reduce((sum, h) => sum + h, 0);
}

// Changes on requests from accounting
calculatePay(): number {
return this.totalHours() * this.hourlyWage;
}

// Changes on requests from the floor supervisor
summarizeHours(): string {
return `${this.name}: ${this.totalHours()} hours`;
}

// Changes on requests from the IT contact
save(): void {
// Persist the record
}
}

The three methods all read the same workedHours, so at a glance they look tightly related. That is exactly where the danger is.

Suppose accounting asks, "please calculate overtime at 1.25×." If you reach into how the total is computed in order to fix calculatePay, the output of summarizeHours, which uses the same calculation, changes too. The floor supervisor asked for nothing, yet the numbers they look at have moved.

Split apart, it looks like this.

✅ Good: split by requester
class StaffMember {
constructor(
readonly name: string,
readonly hourlyWage: number,
readonly workedHours: readonly number[],
) {}
}

class PayrollCalculator {
// Only accounting's requests arrive here
calculate(staff: StaffMember): number {
const total = staff.workedHours.reduce((sum, h) => sum + h, 0);
return total * staff.hourlyWage;
}
}

class TimesheetReport {
// Only the floor supervisor's requests arrive here
summarize(staff: StaffMember): string {
const total = staff.workedHours.reduce((sum, h) => sum + h, 0);
return `${staff.name}: ${total} hours`;
}
}

class StaffRecordStore {
// Only the IT contact's requests arrive here
save(staff: StaffMember): void {
// Persist the record
}
}

Note that the code for totaling hours is now duplicated in two places. Factor the duplication out to avoid it and the coupling you just removed comes back. Accepting the duplication is the right call here.

"Merge it because it is the same code" and "merge it because it changes for the same reason" are different criteria. SRP takes the latter. Even when they look identical, split them if the requester differs.

How it gets misused — splitting is not free

SRP is misused in the direction of following it too far, not of ignoring it. Because the word "responsibility" is subjective, you can find as many reasons to split as you like.

The typical destination is a row of classes that each hold a single method.

❌ Bad: the criterion for splitting has become 'the operations differ'
class WorkedHoursSummer {
sum(hours: readonly number[]): number {
return hours.reduce((total, h) => total + h, 0);
}
}

class OvertimeExtractor {
extract(hours: readonly number[]): readonly number[] {
return hours.filter((h) => h > 8);
}
}

class WageMultiplier {
multiply(hours: number, wage: number): number {
return hours * wage;
}
}

class PayrollCalculator {
constructor(
private readonly summer: WorkedHoursSummer,
private readonly extractor: OvertimeExtractor,
private readonly multiplier: WageMultiplier,
) {}

calculate(staff: StaffMember): number {
// It only calls the three classes in order
const total = this.summer.sum(staff.workedHours);
const overtime = this.summer.sum(this.extractor.extract(staff.workedHours));
return this.multiplier.multiply(total + overtime * 0.25, staff.hourlyWage);
}
}

Who is asking for this set of classes? All of them, accounting. Change the criterion for overtime and OvertimeExtractor changes; change how the hourly wage is handled and WageMultiplier changes — but the party making the request is the same. If there is one actor, SRP does not call for a split.

The split costs you two things.

First, changes no longer stay in one place. To land the single request "overtime starts above 7 hours, and the premium goes to 30%," you end up opening both OvertimeExtractor and PayrollCalculator, which holds the premium rate. Before the split, that change fit inside one method.

Second, the flow of the processing becomes unreadable. To learn how pay is calculated you have to move back and forth across four class definitions. There are more names now, but more names do not by themselves aid understanding.

Martin himself wrote a warning about applying a principle excessively. It is about the interface segregation principle, but the shape is the same.

As with all principles, care must be taken not to overdo it.

The source is the 2000 document2. The phrase "as with all principles" is what matters. That a principle admits a state of being followed too far was the proposer's own view.

When it's OK to break it

Deciding not to apply SRP is sound when the following conditions hold.

When there is only one actor. If the party asking for changes is always the same, nothing collateral happens even without a split. Splitting gains you nothing and adds only indirection.

When the change frequency is near zero. What SRP prevents is accidents at change time. In an area that has not moved in years, there is no accident to prevent. Splitting on "it might change someday" is the same failure as the predictive abstraction covered in the next chapter.

When the indirect cost of splitting exceeds the collateral cost. While the scale is small, the value of being able to read one file and see the whole wins. The evidence to weigh is "does a change stay in one file, or does it always scatter across several?"

In every case, the decision not to split can be undone later. You split when the actors increase. Going the other way, consolidating something split too far takes as much work as the call sites are scattered. When in doubt, leaning toward not splitting makes the undo cheaper.

Where TypeScript stands today — not limited to classes

Martin's formulation says "software module," not "class." He uses the word module in the 2014 article as well.

In TypeScript, the unit of responsibility is not necessarily a class.

It applies at the module (file) level. Even in a file that exports only functions, if the functions gathered there have several requesters, SRP is in play. The criterion is the same: you look at "how many kinds of parties come to this file asking for changes."

The same question arises for type definitions. When one type doubles as an API response shape, the display fields of a screen, and the input for saving, three requesters land in the same type. Add a field for the API's sake and the screen and saving types move too.

❌ Bad: one type with three requesters
type StaffRecord = {
id: string;
name: string;
hourlyWage: number; // Used by accounting
workedHours: number[]; // Used by the floor supervisor
updatedAt: string; // Used by the IT contact
};

Whether to split the type or carve out per-use views with Pick is the next decision. Here we stop at confirming that this is not only about classes; carving out types is covered in the chapter on interface segregation.

Summary

  • SRP's "reason to change" refers not to a property of the code but to the people who come asking for changes. Martin makes this explicit in his 2014 article with "This principle is about people."
  • Counting responsibilities therefore means looking at how many kinds of requesters there are, not at the contents of the class
  • What SRP reduces is not line count but the cost of collateral changes caused by different requests landing in the same code
  • Even when the code looks identical, leave the duplication if the requesters differ. "Merge it because it is the same code" is not SRP's criterion
  • Misuse runs in the direction of following it too far. If there is one actor, SRP does not call for a split
  • The decision not to split can be undone later, but consolidating something split too far is expensive

The next chapter covers the open–closed principle. We look at how the "it might change someday" prediction touched on here fails in the context of OCP.

Exercises

Is the following split an application of SRP, or over-splitting? Identify the actors, decide, and state the conditions under which your decision would change

Code handling a library's loan records is divided into the following two classes.

class LoanDurationCalculator {
// Calculates the loan duration
calculate(borrowedAt: Date, returnedAt: Date): number {
const ms = returnedAt.getTime() - borrowedAt.getTime();
return Math.floor(ms / (1000 * 60 * 60 * 24));
}
}

class OverdueChecker {
constructor(private readonly calculator: LoanDurationCalculator) {}

// Determines whether the loan is overdue
isOverdue(borrowedAt: Date, returnedAt: Date, limitDays: number): boolean {
return this.calculator.calculate(borrowedAt, returnedAt) > limitDays;
}
}

Sample answer

With only the information given, it could be either. The decision rests on the number of actors, and that cannot be read off the code.

When it counts as over-splitting: if how the loan duration is counted and the criterion for being overdue both change on requests from the same party (the library's operations staff, say), there is one actor. SRP does not call for a split in that case. Because of the split, the single request "the loan duration excludes the return date, and overdue starts on day one" means opening two classes.

When it counts as an application: if how the duration is counted is determined by fiscal-year or statistical needs (a request from accounting or from whoever owns statistics) and the overdue determination is set by operating rules aimed at patrons (a request from operations), there are two actors. In that case leave the split in place. It prevents one party's request from changing the other's behavior.

The additional information needed to decide: "over the past year, when either of these two was changed, who made the request?" If there is a change history, you can actually check whether the requesters are separate. If there is no history, leaning toward not splitting and splitting once the requesters diverge makes the undo cheaper.


Footnotes

  1. Source: The Single Responsibility Principle (Robert C. Martin, 2014-05-08). Every quotation in the body comes from this article. The article uses as its example an Employee class with three methods for payroll calculation, hours reporting, and saving; the code examples in this chapter transpose that structure onto a library's staff records.

  2. Source: Robert C. Martin, "Design Principles and Design Patterns" (2000), the section on the interface segregation principle. For the PDF referred to, see the footnotes in chapter 1.