The Primary Objective of Software Design: Minimizing Total Cognitive Load
20 May 2023Over my half-career in software development, I’ve started to collect some insights (or at least opinions) about how software can be built so that it is easy to maintain, use, and extend. Usually we hear of principles such as modularity, abstraction, loose coupling, and separation of concerns, and each of these is important to strive for. But I’ve found that behind all of these, there is a single, unifying principle – the reduction of cognitive load. In this post I talk about what I’ve come to think of as the primary objective of software design: minimizing total cognitive load of all future users and maintainers of your software.
What causes cognitive load?
Humans have a limited capacity to pay attention to a specific topic and humans are limited in the amount of content they can carry in their minds at any one point in time. Therefore, when designing software, one must be careful to remain within the bounds of human capability.
Consider attention. Even little things can steal our attention away. Do you like writing code with single character variables? (Are you a golang programmer? 😂) When future developers read your code, they will spend time looking for the declarations of these variable. It’s not much time, but it’s time spent outside of the task at hand. And if you have several such poorly named variables, then the reader must keep a mental cheat sheet of all these variables and their definitions. This is why I tend toward longer and more descriptive variable names. If I can skip a reference check then I’ve kept the developer in the game just a bit longer. And the only price I pay is a few more keystrokes (and occassionally being the butt of jokes about my long variable names).
There are bigger, more obvious things that steal our attention. If code is confusing, or if I’m using an unfamiliar or poorly crafted API, then I spend a lot more time reading and figuring stuff out. I’ll look at comments, I’ll trace code and figure out what it really is doing, I’ll look for other examples of code that are doing something similar. If I have to, I’ll look for documentation (and usually find that, sometime since their writing, it has all become well crafted lies). If code is intuitive and easy to understand, then sometimes, these deep references can be avoided.
Another thing I always rail against is “clever” code. I write my code like a 10 year old writes their school essays, simple and to-the-point. This means I leave out “cool things” like meta-programming, domain-specific-languages, context-managers, and friends unless they’re absolutely called for. I even leave out perfectly respectable, commonplace approaches like functional programming if I know that the future maintainers of a codebase aren’t familiar with these approaches. The goal is to avoid having the future developer stop their train of thought in order to figure out what this arcane spell you casted in code is really doing.
Now, let’s consider cognitive capacity. How much can we hold in our heads at once? Have you ever opened up too many browser windows? (Are you a modern human?) What do you do? You suffer it for a while. You might open a new window that is a duplicate of some other tab you can’t find again. You’re computer might get slower if you’ve consumed too many resources. Eventually you just declare bankruptcy and close all the tabs and start over. This feeling is exactly what we want to avoid in our software development. This means that whenever a developer is trying to do work, we must them to hold in mind only the minimal amount of information necessary. Consider our list from the lead-in paragraph. Each of these can be viewed as a ploy to reduce cognitive load:
- modularity - If a code base is well modularized, then future developers need only consider the portion of code they are changing. If there is no modularity, then the user must hold the entire codebase in mind at once.
- abstraction - If an API abstracts away unnecessary implementation details then a developer need only to use the API without worrying about how it’s implemented. But if the abstraction “leaks” then the developer finds themselves thinking at the same moment about the problem they are trying to solve and about the details of a poorly architected interface.
- loose coupling - If two (or more) modules of functionality are able to function completely independently then you can reason about them in isolation. But if something connects their behavior, then the developer must hold both modules in mind at once.
- separation of concern - If each module of functionality performs one and only one task well, then that task can be quickly and thoroughly understood. But if a module acts more like a Swiss army knife, then the developer must consider a variety of functionality at once in order to make sure they don’t get cut by the other blades!
Optimizing to remove the right cognitive load
Life is about tradeoffs, and good software design is no different. Irreducible complexity must live somewhere in your codebase but you often get to choose where. This is where my philosophy of minimizing total cognitive load becomes most easy to articulate. When writing code, think about the developers who will interact with your code. Let’s say that you are building a new internal API that will live in an existing codebase. There are three software entities here, and three types of developers:
- The API itself, (e.g. not the implementation of the API, just the exposed functions, structures, constants, etc.) and the developers who make use of the API.
- The codebase and the general developers of the codebase. These developers will read through code that makes user of the API, though they will not necessarily have to interact with it.
- The implementation of the API itself, and the developers who have to maintain it going forward.
Let’s consider a simple example. Let’s say that you are working on an event recommendation app - something that recommends rock concerts, festivals, classes, meetups, etc. You wish to create a new API for an event that generalizes the notion of an event. A Muse concert is a very different type of an event than a yoga class, and so they will be displayed differently, but in most other ways they should be treated uniformly in the codebase. So your 3 domains here are the API of the Event class, the recommendation app code base (which includes many mentions of events), and the implementation of the Event class itself.
Now, of the three corresponding groups of developers, who will be most affected by the decisions you make as you build this API? Often, developers will create new event types, and interact with the events through the codebase - these are likely the most affected. Second, there is lots of development in the recommendation app codebase that doesn’t directly involve events (things like tickets, or finance, or messaging attendees and organizers, etc.), but developers working on things besides events will nevertheless regularly read through code that deals with event. Finally there is you, the person developing the API and similarly the people that are on the hook to maintain it going forward.
An all-too-easy pitfall here is to identify most closely with the future developer that is maintaining the API that you’re building. It makes sense; you, after all, are likely to be that developer. But the maintainer of the API is often the least affected party. “How can that be? All that support work! All that maintenance! I know that I’m affected by the API.” True, but there’s two things to consider here. First, you might have lots of maintenance work, but you are one person (or you are one team depending on the size of the API). You are not the large percentage of application developers that use the API regularly. Aggregated over the number of developers, those using the API are almost definitely the most affected by the API. The maintainer group is also not likely to be the 2nd place, because a broadly used API that is encountered by all of the application developers is likely to affect them significantly even if they are only reading over code that uses the API.
So should we worry about reducing the cognitive load of the maintainer? Absolutely! Make the implementation as simple as possible – but only to the extent that the complexity of the other two groups remains unaffected. By the very act of deferring to the other developers you reduce the burden of support and maintenance by ensuring that no one is on your back about how to use the API or how to change it so that it’s easier to understand.
conclusion
The main takeaway is to consider which group of users are most affected by your software design and to the extent possible reduce their cognitive load first. Typically this means the end user of the API first, and (somewhat paradoxically) maintaining engineers last.
How do you do this? If you just follow good software principles (modularity, abstraction, loose coupling, and separation of concerns, and all the rest) then you will go far in decreasing the cognitive load of your developers. But cognitive load serves as a good framework to keep in mind because it allows you to empathize with the future developer and it serves as a rationale for why we should work towards all those software design principles in the first place!