cat posts/2024-12-24-cognitive-load-is-what-matters.md

Cognitive load is what matters

· 4 min read · leadership teams software-design

Four. That’s roughly how many things a person can keep in working memory at once, according to the research Artem Zakirullin cites in his essay Cognitive load is what matters, which I’ve seen linked several times this year and finally read properly over the weekend. (The older number you might know is Miller’s “seven, plus or minus two”, from 1956. The more recent work puts it lower, nearer four chunks, and I find four much more believable when I think about how I actually read code.)

The essay’s argument is simple and I mostly agree with it. When you read code to change it, you’re holding things in your head: what this variable means, which branch you’re in, what the caller assumed, what this base class does that the subclass doesn’t override. Some of that load is intrinsic to the problem, and you can’t make tax rules simpler than they are. Most of it, he says, is extraneous, created by how the code is written, and that’s the part worth fighting. He goes through the usual suspects: nested conditionals that could be early returns, a condition with five clauses that could be three well-named variables, deep inheritance, too many tiny modules with complicated interfaces, too many microservices, clever language features, and frameworks with a lot of magic in them.

Very little of this is new, which he’d be the first to say. The “shallow modules” section is Ousterhout, and the part about splitting code into too many small pieces is what I complained about in the Clean Code post, the fifteen three-line methods called HandleInternal. What’s new is the framing. “Is this clean?” is a question people argue about forever. “How many things do I have to hold in my head to change this?” is a question you can nearly count.

Counting

I tried it on a piece of our own code last week, which I’d recommend, because the number is always higher than you’d guess. One of our .NET services handles invoice adjustments. To understand where a refund amount comes from, starting at the API controller, I had to keep track of: the controller’s mapping from the request to a command object; the command handler; a “policy” class chosen by a factory based on the account type; the base policy class, because the chosen one overrides two of its five methods; a rounding helper with a flag that changes its behavior; and the repository, because one of the values is computed in the SQL. That’s six things, and I gave up holding them at about four and started writing them on paper.

Every one of those pieces was added by someone sensible, for a reason. The factory came in when we added a second account type, the base class when we added a third. Each step made the code a little more “extensible” and a little harder to hold. Nobody ever decided to make it hard; it just never got anyone’s attention as a cost.

Teams have working memory too

What I’d add to the essay, because it’s mostly about code, is that the same limit applies to teams, and that it’s a much more useful way to think about org design than the boxes on an org chart.

The book that made this click for me was Team Topologies, by Matthew Skelton and Manuel Pais, which treats a team’s cognitive load as the main constraint on what it should own. A team can only really understand so many systems. Give it more, and it doesn’t fail visibly. It just starts to own some of them nominally: nobody on it knows how they work, changes to them are slow and nervous, and incidents in them take twice as long because the first hour is spent remembering.

We have about twenty engineers. In the spring, one team of five owned nine services, from the API gateway to the PDF reports to a scheduler that runs the recurring jobs. (That’s the scheduler with the timezone bug, which sat as “can’t reproduce” for months, and I don’t think that’s a coincidence.) Each of the five knew three or four of the nine well. On-call for the team meant being paged for all nine. When I asked them, in a retro, to list for each service who could fix it alone at 2 a.m., two of the services had nobody.

So we moved three of the services to other teams, picked by which team already changed them most often, and retired a fourth by folding it back into the Rails app it had been split out of in 2019. That took most of the summer and wasn’t glamorous work, and I’d do it again. The same retro question asked again in November had a name against every service.

Where I disagree

The essay is a little too sure of itself in places, which is part of why it’s fun to read. Familiarity does reduce cognitive load, whatever the purists say. An idiom everyone on the team knows is one chunk for them and four for a newcomer, and the essay’s answer, that you should measure confusion from new hires, is good but incomplete. Sometimes the right move is to teach the idiom rather than remove it. A Rails developer reading an ActiveRecord scope isn’t paying for its magic, they learned that cost years ago.

And some intrinsic complexity hides as extraneous. The invoice code above has a rounding flag because two of our account types really do round differently, for contractual reasons. You can name it better, you can move it, but you can’t make it go away, and a refactor that pretended otherwise would just move the load somewhere less visible.

Still, as a question to ask in code review, “how many things do I need to hold in my head to change this?” is better than most of the ones I’ve used. I’ve started asking it of the team structure too, every time someone proposes a new service.