Single Responsibility Principle
Every thing should do one thing and one thing only.
A function should do one thing. When two concerns share one body, changing one breaks the other. Split them while the seam is obvious.
Wait six months and the seam is no longer obvious. The next person to touch the file learns it as one unit, and the bad shape becomes “how this thing works”. The cost of getting this wrong is not the bad function: it is the code that grew up assuming the bad function was a fact.
Opinion
If you take one thing from this entire site, take SRP. It is the keystone tenet, the one every other Foundation hangs off, and the one I've never had to argue. Every developer I've worked with agrees in principle: a function should do one thing. The argument is always at the example: whether processOrder is one thing or six, whether validateAndSubmit is two jobs sharing a name. The seam, not the rule. That is what makes SRP load-bearing for the rest of the canon. Once a function does one thing, it can be named for what it does (F2 Intention-Revealing Names); the comment stops earning its keep (F5 Self-Documenting Code); duplication becomes visible and F3 DRY turns it into a refactor; complexity stays inside the budget F4 Simplicity draws; the test surface narrows, which is what P1 Test First rewards; TS6 Behaviour Testing needs only one assertion to cover one behaviour. The mixed-concern function pays a tax at every call site: someone re-derives intent from the body because the name no longer covers the work.
Read what the code is doing. If the description needs and then, the function is the wrong size. Extract the second job, name the extraction, and follow A2 Three-Tier Hoisting into the right module. The same logic scales: A4 Common Closure Principle is SRP at the package, and S2 Cyclomatic Caps is the linter that forces the conversation when judgement falters. Cut the debt as it appears, not when it accretes.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Every method or function should do one thing only. Separate concerns out to make everything else easier. /tenet/single-responsibility-principle/F1
AI eyes only
Rule: one function, one verb. One reason to change per unit.
Reject: function or method names containing “and”, “or”, or “then”. Reject: a handler that parses, validates, persists, and formats a response in one body.
Generate: separate functions for input validation, business transformation, persistence, and response shaping. Each takes typed input and returns typed output. Each is independently testable.
Diagnostic: if the function needs “and then” to describe, extract until each step has a name. Length is not the constraint; count of reasons-to-change is.
Why?
- Easier to name. A function that does one thing can be named for that thing. A function that bundles several jobs gets named for one of them; the rest are invisible from the signature. See F2 Intention-Revealing Names.
- Easier to test, maintain and modify. A single-purpose function has a small surface, so test setup stays small, edits stay contained, and there are fewer ways to break what was not changed.
- PR reviews are faster because the code is easier to read. One thread of logic per function scans quicker than mixed concerns tangled in one body.
- The right home becomes visible. Once a unit is abstracted to a single responsibility, it is obvious whether it belongs in the local module, the product folder, or the generic folder. Buried inside a multi-purpose function, the same logic stays where it does not belong. See A2 Three-Tier Hoisting.
- Existing code surfaces. When a function is lifted to where it belongs, an equivalent function may already exist there — the existing version is reused instead of duplicated. See F3 Don't Repeat Yourself.
- Smaller diffs. A change to a single-purpose function affects only that function. A change to a function with mixed concerns ripples across the file.
- Easier onboarding. A single-purpose function explains itself from its name and signature. A multi-purpose function needs documentation that often does not exist.
Origins
Robert C. Martin popularised the SRP acronym in Clean Architecture (2017). The formulation given there is sharper than “do one thing”: a module should be responsible to one, and only one, actor, and the reason a module changes is the actor whose interests it serves.1Clean Architecture (2017): SRP as one actor / one reason to change — the formulation that replaces vague “do one thing.” The actor framing is testable in review where “do one thing” is not.
The idea is older. David L. Parnas's “On the Criteria to be Used in Decomposing Systems into Modules” (1972) does not use the phrase “single responsibility” but states the same cut: list the design decisions likely to change, then give each one a module that hides it.2Communications of the ACM 15 (12), 1972 — information hiding as the criterion for module boundaries. SRP is the same boundary with different vocabulary when the hidden decision is “who pays when this changes”, not only which data structure stays private.
Practitioners arrived at the same place independently. The Linux kernel coding style §6 states it directly: functions should do one thing and do it well.3Linux kernel coding style §6 — functions should do one thing and do it well. Steve McConnell's Code Complete (2004) links the habit to functional cohesion in routines, where unrelated work in a single routine is a common source of regression.4Code Complete, 2nd ed. (Microsoft Press, 2004) — cohesion in routines as single-purpose work. Martin Fowler's Refactoring catalogues the operational techniques: extract what does not belong, rename until the comment is redundant.5Refactoring, 2nd ed. (2018) — extract and rename as the mechanical response to mixed responsibilities.
Eric Raymond's Rule of Modularity in The Art of Unix Programming states the same constraint at the system level: small components with simple interfaces.6The Art of Unix Programming — Rule of Modularity: simple parts connected by clean interfaces. The SOLID acronym ordering is pedagogical, not architectural; Fowler has cautioned against treating five rules from one author's bookshelf as a settled framework, and they remain useful because teams still drift off the basics.7martinfowler.com/bliki/SolidRelevance.html (2020) — SOLID as teachable basics, not physical law. Prickles leads with SRP because every other Foundation depends on this boundary holding.
Quotes
A module should be responsible to one, and only one, actor. The most pernicious violations of this principle come from accidentally entangling two actors' concerns in the same code.
Functions should do one thing. They should do it well. They should do it only.
We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01Clean Architecture — SRP chapterSupportsOne actor, one reason to change — the version you can actually defend in review instead of hand-waving ‘do one thing.’
- 02Hide volatile design decisions behind module walls; SRP’s older, more formal cousin.
- 03Treats single-purpose routines as the default for code people can still read years later.
- 04Unix doctrine in one line: keep parts small and the wires between them boring.
- 05Extract and rename when responsibilities tangle — the mechanical moves people actually run.
Martin and McConnell talk craft; Parnas and Raymond talk structure; Fowler catalogues refactors and why “SOLID” is not magic; linters enforce rough size and complexity caps; Sandi Metz and the qntm essay warn what happens when the rule becomes a religion. None of that replaces the actor test in Clean Architecture1Clean Architecture (2017): SRP as one actor / one reason to change — the formulation that replaces vague “do one thing.”.
Examples
// Before: one function parses, validates, scales, and saves.function syncScaledCounter(raw: string): number { const row = JSON.parse(raw); if (row.n === undefined) { throw new Error("missing"); } const scaled = row.n * 2; localStorage.setItem("counter", String(scaled)); return scaled;}
function getCounterOrThrow(row: { n?: number }): number { if (row.n === undefined) { throw new Error("missing"); } return row.n;}function persistCounter(value: number): number { localStorage.setItem("counter", String(value)); return value;}export const syncScaledCounter = (raw: string): number => persistCounter(getCounterOrThrow(JSON.parse(raw)) * 2);
Trim in one function, persist in another, format the response in a third: three verbs, three names, three tests. The enforcement tabs list how each language approximates the same mess with complexity and length rules. Those are proxies; someone still has to say what the job is.
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-lines-per-function | ESLint | functions that have grown past a single readable unit — the symptom SRP asks you to split. |
| complexity | ESLint | cyclomatic complexity over threshold — too many independent paths in one body. |
| max-statements | ESLint | too many sequential statements in one function — often parallel concerns stacked. |
| max-params | ESLint | argument lists long enough to signal a missing parameter object or a function wearing two hats. |
| max-depth | ESLint | deeply nested control flow — extraction usually reveals separate responsibilities. |
| unicorn/consistent-function-scoping | eslint-plugin-unicorn | nested functions that are only used once — push down scope or extract; the fix often splits responsibilities. |
eslint.config.mjsconfiguration snippet
// Prickles-style flat config (excerpt) — SRP proxies in eslint.config.mjs
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'max-lines-per-function': ['warn', { max: 15, skipBlankLines: true, skipComments: true }],
'max-lines': ['warn', { max: 150, skipBlankLines: true, skipComments: true }],
complexity: ['warn', 3],
'max-depth': ['warn', 3],
'max-nested-callbacks': ['warn', 3],
'max-statements': ['warn', 10],
'max-params': ['warn', 3],
'unicorn/consistent-function-scoping': 'error',
}
}AI rules
.cursor/rules/f1-single-responsibility-principle.mdc---
description: Prickles F1 — Single Responsibility Principle
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles F1 — Single Responsibility Principle
One obvious job per function, class, or module. When a second concern shows up in the same unit, peel it out so each piece has its own reason to change.
Names should read as a single verb phrase. If you need “and” to describe what the function does, you probably have two responsibilities—extract until the sentence holds without a conjunction.
Say no to god procedures: parsing, validation, persistence, and response shaping live in different places unless one real task honestly spans them.
Treat max-lines, complexity, statement count, and parameter caps as linter smoke alarms, not definitions of SRP. They catch tangles early; they never replace asking who pays when this changes.
If a patch drops a whole feature into one long routine, bounce it until decomposition by verb is done and each slice has a name you can defend.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strong objection is familiar. Taken literally, SRP produces ravioli: walls of one-liners, names that parrot the type system, and a stack walk that needs a diagram. Sandi Metz pinned the price of the wrong abstraction: live with honest duplication before paying for a split that has not been earned.8“The Wrong Abstraction” (2016) — duplication is cheaper than the wrong abstraction; split on evidence. John Ousterhout's deep-module argument is the same worry from another angle: shallow types swap one source of confusion for another when the interface does not actually hide anything.9A Philosophy of Software Design (2018) — deep modules vs shallow classes; interface power over fragment count.
That lands. The wrong lesson is “responsibilities do not matter.” The right one is “pick the seam with care,” which is the whole point.
Counter-argument retort
SRP was never “make everything tiny.” It was always “make each unit answer to one actor.” A function within the line cap with one reason to change stays. A function that schedules vet visits, totals donations, and emails volunteers because the ticket said “do the weigh-day handler” goes, however few lines it fits into.
When Sandi Metz warns against the wrong abstraction8“The Wrong Abstraction” (2016) — duplication is cheaper than the wrong abstraction; split on evidence., the argument is for evidence before extraction. That lines up with S2 Rule of Three: see the echo, then cut. When Ousterhout asks for depth9A Philosophy of Software Design (2018) — deep modules vs shallow classes; interface power over fragment count., the call is for interfaces that swallow real complexity, not a directory full of stubs. Both curb eager splitting. Neither is permission to weld unrelated concerns together because it was faster yesterday afternoon.
Day-to-day test: who signs off on the change? Two names means two jobs. Split until the name reads like a verb the reviewer recognises, let the linters nag about size, ship. F5 Self-Documenting Code and F2 Intention-Revealing Names keep the seam obvious after the warnings go green.
Notes
- [1]Robert C. Martin — Clean Architecture (2017): SRP as one actor / one reason to change — the formulation that replaces vague “do one thing.”
- [2]David L. Parnas — Communications of the ACM 15 (12), 1972 — information hiding as the criterion for module boundaries.
- [3]Linux kernel community — Linux kernel coding style §6 — functions should do one thing and do it well.