EnriqueMark
Design Patterns & Architecture

Single responsibility

2026-07 English

My heuristic reading of single responsibility (SRP) is “unit-testable, no side effects, a function does one thing only”, and I know that isn’t fully rigorous. But as a quick judgment I think it’s a good one.

Unit-testable. It means the function doesn’t depend on outside state, has no extra rules, and can be tested in a pure setting, which usually also hints that the function has already been separated out to a fair degree.

No side effects. This one isn’t always absolute. Some functions have side effects as their legitimate responsibility, like writing a log or querying the database. The core point is that unless this really is that function’s legitimate responsibility, it shouldn’t reach into things another function was supposed to do, and delegation is the best move there. That is, delegate to a function whose job is that thing, rather than doing it yourself.

Does one thing. The “one thing” can’t be taken literally, you have to think about the level of abstraction. At a higher level of abstraction, “one thing” can cover a lot, and it’s more like a “responsibility”. Responsibility is a fairly abstract thing to begin with, and the way I read it is as a job function, the smallest single capability this function provides in a given place.

The clearer SRP signal is actually change. Like, when I modify it in the future, am I only ever going to change it for one “reason”? Or more precisely, when I change this function I won’t end up changing something unrelated, whereas changing a function that queries the database and having to watch out that I don’t break an HTTP request is responsibilities mixed together. It’s exactly because different things got mixed in that you come back to change this function for different reasons. And the flip side is that a function like this is also the hardest to unit test, because to test it you have to build different environments. So “split code by the reason it changes” is a stronger SRP signal, and the key is the maintenance boundary.

There’s another kind of function whose responsibility is orchestration. For that kind it’s like the final executor, packaging up several functions with different responsibilities and running them. Note that business logic and orchestration are not the same. Orchestration just runs things in a particular order, while business logic carries all kinds of state control and branching, and for the latter the responsibility isn’t clear at all. It’s delegation either way, but if things other than plain execution get mixed in it can turn into a wrapper, which at bottom only threw the business logic outward while everything is still coupled.

The key is state. If the state is someone else’s, then this piece of the responsibility is best returned to where it happens. If a chunk of code is mostly handling someone else’s state, it’s a sign it should be split and refactored to wherever that state belongs, which means the current code is coupled, coupled to state elsewhere. Feature Envy is exactly this kind of bad smell, spending all its time handling another object’s state. If there’s only one such party, this whole chunk should move over there. If there are several, there are two cases: 1. the several change together, and change for one purpose, always in the same combination, and none of them can be left out, which means this chunk is a single responsibility of its own and should get its own method or object; 2. the several change separately, so they have to be split out and exist as separate methods. And then there’s pure sequencing, a fixed lifecycle say, rather than state operations that go together, and that’s the orchestration we talked about above.

The distinction comes down to responsibility. If some responsibility already has an owner, or doesn’t have one yet, then that owner should be the one doing it, rather than some other method with a different responsibility doing it. The point is telling three things apart: delegation, envy, and orchestration. One part here is hard to pin down, which is “its own state”, and what is that exactly? The core of it is that the state that always goes together and gets operated on repeatedly is the “self”. That “self” may not exist before we wrap it up, but once it’s packaged you can hand all that state over to it to manage as a unit, and from then on what it modifies is its own state, which it’s responsible for. When I’m not just doing a single operation here (that’s delegation) but doing a whole flow (a set of operations) every time, that’s a sign I may have crossed a line.


Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: 单一职责.