Stroopy Software
Even the best idea in a software system can sometimes turn into a hurdle that consistently trips the team up. Perhaps it's the most innovative design pattern, or the trendiest framework. Maybe you deliberately employed time-tested techniques from a decade ago due to their reliability, or you developed everything independently because, quite frankly, you're highly intelligent.
What's unfortunate is that in any one of those cases, you can easily end up with a software system where developers often need to pause and reevaluate their actions, questioning the potential for catastrophe before proceeding. Sometimes, they don't even realize they need to reassess and they accidentally create something incorrect.
In essence, your architecture has developed a Stroop effect.
The Stroop test is used to highlight the slowdown in cognitive processing when confronted with two mismatched pieces of information simultaneously. Below is the classic example:
As quickly as possible, say the name of each text colour out loud, but do not say the written word.
Next, do the opposite: say the written word, but not the colour of the text.
The Stroop test demonstrates that inconsistent information, especially when intertwined, can boost errors and slow cognitive processing. Even if you were familiar with the Stroop effect beforehand, you likely had some difficulty with the test above. There’s a lesson here about how our brains are wired and what we’re good at: crafting compelling narratives with relevant details, as opposed to dealing with disjointed attributes.
Sometimes software can be written like a Stroop test: continual mental exhaustion, prompting constant second-guessing, resulting in unintended mistakes, and setting numerous hidden traps. A strong indicator you're dealing with a Stroop Architecture is when everyone hesitates to modify the code, getting mentally drained by even relatively simple alterations.
Where the Stroop Effect manifests itself
The Stroop effect in code is harder to spot than colour names in text. Code is a dense network of logic designed to translate into real-world value, so naturally it will be more complex than the original Stroop Test above. The effect however, is still here.
The most obvious one: Bad comments
Bad comments are all too familiar. The moment a comment becomes irrelevant or misleading, the Stroop effect kicks in. I think this is why a lot of people argue for “code that explains itself” to be honest; people are bad at updating two sources of information when one (the code) “does something” and the other (the comment) is “decorative”. The only way to avoid this in your code is a strong development culture that has standards on comments and enforces that culture through reviews, fixing broken windows when they see them, or holding each-other seriously accountable for it.
The moment someone updated the code and allowed a relatively unimportant comment to remain unupdated, the stroop effect kicked in.
I first encountered these 4 lines of code a decade a go, and to this very day they still live rent-free in my brain:
break; default: // Run for your lives! }
The author left the company 2 years before I touched that code and nobody around knew what it meant. I wasted several hours looking through every possible edge case to understand if I was going to break the system or this was just an outdated Dr Who reference and the author thought it would be cute.
This isn’t only about lines of code.
Consider your current infrastructure and all the catchy titles of your systems. For instance, how the "Starship" service interacts with the other team’s "Big Papa" service via the "English Chunnel". It feels like teams intend to make their code's purpose confusing.
Reflection: You always did your best. How many Stroop tests have you left behind in things you created? How much time and resources will your software team expend deciphering these tricky annotations this year? How many days of code tracing and unintended service disruptions has your development career yielded due to these mental speedbumps left in your wake?
Overriding meaning (code structure)
Back in the 70s programmers would heavily use “bit flags” to signify special meaning. It meant taking an integer and assigning special meaning to each bit in the string. Imagine an 8 bit data type where all flags are off: 00000000, and maybe two special flags get turned on in another version: 00101000 (1052672 in decimal). The programmer would pass around the number “1052672“ and inspect the bits to determine special meaning. This still can make some sense in high-performance applications, but 99+% of developers really have no reason to use this technique in 99+% of their tasks. I suspect the majority of web developers look at this and think “wow that’s archaic looking, we would never do that“, but then they proceed to pack meaning into nondescriptive arrays this:
data = [ [ // Tab 1 5, // user ID "Bob", // User name 55 // User age "Acme Co" // User Company ], [ // Tab 2 9, // user ID "Bobbett", // User name 35 // User age "Acme Co" // User Company] [ // Tab 3 "Not a user" // We added this a year later, it is a special flag to indicate this tab is not a user but a company profile "94" // Company ID "Acme Co" // Company Name "2" // Active Sales "Sammy" // Sales Contact ] ];
And now to access the data we have to encode all the meaning somewhere else:
if (data[tab_identifier][0] !== "Not a user") { return data[tab_identifier][user_data_field] } else { return data[tab_identifier][company] }
And now you have to duplicate this logic every single place you interact with this data, and when you debug the data you have to keep that logic loaded into your brain.
Commonly as that code grows, one developer sees that the code is complicated and we have this complex logic everywhere, so they pack it all into a common function:
function determine_record_type(record) { if (data[tab_identifier][0] !== "Not a user") { return 1 // 1 means it is a user } else { return 2 // 2 means it is a company } }
And now our code elsewhere looks like this:
if determine_record_type(data[0] == 1) { // Users return data[0][user_data_field] } else { // Companies return data[0][company_data_field] }
When there's a new feature to be integrated, someone must sift through layers of implied and undefined motives to discern the purpose. They need to ensure they're not breaking anything for users or corporations and adhere to potentially conflicting guidelines consistently. When this programming style is expanded to more code areas, you can end up having a disproportionately convoluted model in your head, where one slip or overlooked detail can have significant consequences.
There are numerous ways to address these issues, and most commonly by using some simple objects. Unfortunately, those who default into this from above style are often outspoken critics of anything that looks like Object Oriented Programming, and any approach that wasn’t the one that felt intuitive to them at the moment of writing code. The arguments I often hear argue that OOP is overly intricate and they would rather use arrays (or a “JSON Object”, maybe a “dict” in python) with specially designated positions and one of those text fields is used to identify the object type. This improvisational approach just emulates the pre-existing OOP features in a lacking or faulty manner. Yet, we encounter many codebases with this pattern because developers prefer introducing challenges for their peers rather than employing a simple data structure (a mechanism devised to arrange their data).
Fixing this stuff isn’t hard, but it’s far from the default mode of programming for many developers, and often they exaggerate the downsides of if they were to apply these techniques. Dropping these small Stroop tests for everyone feels more comfortable because it is a comfortable norm. At the same time, the OO community is a lightning-rod for loud mouthed “only my way is right“ people trying to show off how smart they are by using every tool available to them, so the tools available here are very unattractive to people who fall into this trap, and those who tried are dragged into discussions of abstract base classes, interfaces, and inheritance right from the start… often an endless cycle of “no you still aren’t doing it perfectly”.
I’ve seen my fair share of OO Stroop tests in my time too: Message publishers that extend a message base class. One class that holds a method for everything a developer managed to program that week. Task registers that would not only register a task but kick off multiple worker threads and consume messages from queues, (even when instructed not to via config). I’ve seen it all. People who use objects can make just as big of a mess. because the mess was never about tools being used, but about how those tools are being used.
One of the more common code stroops is a long chain of pointless indirection (often just forwarding a call and re-mapping arguments), especially when the names of redirected entities change without a really good purpose. As you go along the chain first thinking you are dealing with a “person” and somehow that becomes a “customer” and eventually a “user”, and you aren’t sure any more if they’re the same concept or three different ones, or what the nuance is.
If you drop in two different ideas at once to describe one thing, you created a Stroop test.
Overriding meaning (Domain structure)
Perhaps best of all, the industry has been “stroopified” under the edict of “DRY“ (“DON'‘T REPEAT YOURSELF“) code. It sounds great on a laptop-sticker, but in reality it leads to really fragile designs that create fear of change. Imagine the classic scenario where there’s a report needed by the support team to determine how many calls took place to manage team performance. Now imagine the finance team also needs this report because the support team is paid per customer call resolved. They’re pretty similar, and so in the pursuit of DRY both business cases get one report query.
Fast forward one year. A different developer needs to work on this query because the support team wants to measure not only calls resolved but also calls unresolved, hangups, time on calls, etc.
What will happen next?
Will the developer notice that the finance team also uses this report?
If not, will the finance report break and nobody gets paid for a week?
If they do, will the developer push back and refuse to change it because finance needs that report?
Will the dev just wait for the stakeholders to have a proxy-war through waged through tickets that describe “how the report should work” ?
…Or will the developer finally break the curse, and create two reports, each with a specific purpose and very intentional name? (Assuming the dev team doesn’t push back and insist it is the worst option because it duplicates code).
And it’s not just about code, it’s about your mental models too. In much the same way two specific business case can get over-loaded on one specific block of code, we can also end up with one specific business model trying to work on a business model that was never built for it.
Imagine you ran a pizza delivery SaaS that handled pizza orders and tracked delivery from pizza shop to a household. You have a friend who works at an internet provider subcontractor, and they run a fleet of technicians who go from house-to-house setting up internet connections for new customers, and they want to use your order management system plus the GPS tracking to manage their entire fleet of service techs.
Now your business is a Stroop model, it looks like a pizza delivery business but it’s a fleet management system that works on a very different booking model than pizza delivery. Things can be similar, for example the delivery driver might make multiple customer visits before going back to “home base”, they have windows of delivery expectation, they all book delivery windows. but somehow the nuances of the models are glossed over and nothing really works the right way as you need to grow deeper into both the complexity of the business and manage the edge-cases that only come up at scale. They’re just different businesses that now run on a model that’s a poor compromise of both and not really getting the job done the right way.
You’ll know you got here when all of your staff are modeling the business (correctly) outside of your software solution, using paper or excel sheets to get everything done. Later on they’ll spend a few hours dumping it all back into your system because they’re told they need to use it. Be careful because this is yet another Stroop trap: It can look like the business actually uses the software to run the business (instead of the small empire of google sheets in the background), leading to the easy mistake of thinking you can change the business by changing the software. The work is not the software, but if you labeled the software that way in your mind: it’s yet another Stroop trap.
Poor organisation of concepts are stroopy too
I’m biased. I like projects that organize code that tends to change together all in the same parent folder, and code that doesn’t change together goes elsewhere. This can be as simple as putting the unit test file in the same folder as the actual file under test (“co-locating tests“), which lets me avoid having that stroopy moment when I import something or mock something (“ugh is that import path correct in this case? Was that import ../tests/mocks or ../../mocks ?“ … import error, import error, “oh right it was ../test/mocks in this folder”).
But things can be even more abstract than that. Your concept for taking orders management could get packed full of logic for other downstream activities like order fulfillment that it bubbles together into one blob with two concepts and one name. Or worse, one name and all business cases share the same files.
The easiest Stroop test to see is the “code-model gap” which is discussed in depth by many others. It describes that the mental model of how code works can be very different from how code is structured, run, written, or even named. Unfortunately, many frameworks set you up to have a large gap between how the code works and how the model works, but you can still take care to co-locate concepts that change together instead of spewing them over different folders based on what job something has in the architecture (“routes go in the route folder, controllers go in the controller folder“ being a way to group things that rarely don’t change together, but much beloved by many).
In conclusion
Stroop traps become stronger when we do 3 things together: increase the gap between two interwoven concepts that are also in the same space.
Check how many conflicting concept you are packing together. Will you get it perfect? No. Never. Will you reduce errors and slowdowns at an order of magnitude that goes far beyond your own personal contributions? Yes!
Just, please, don’t make everyone take a constant stroop test every moment they are working on your stuff.