Nudging and boosting complex systems
What makes software complex and how we can nudge and boost complex systems towards patterns of operating that we desire.
In What do Quality Engineers do?
Each sub-system will interact with others, producing behaviours that are often hard to predict or replicate. And it's this interaction of sub-systems that leads to software being a complex process.
In this post, I'd like to expand on what makes software complex and how we can nudge and boost complex systems towards patterns of operating that we desire.
What is complexity?
We often see things like cars and software as complex. Things that are not simple but understood and knowable. For instance, how a car combustion engine works is well understood. Software is similar, and all languages must follow a specific syntax and be written in a particular way to compile and execute. But what is complex for one person might be pretty straightforward to another, so we need a shared understanding of what complexity is.
This is where we can borrow some ideas from the Cynefin framework (pronounced ku-nev-in). Going into Cynefin is beyond the scope of this post, but Cynefin does something very interesting with defining complexity.
Complex and complicated
We often see complex and complicated as synonyms, but Cynefin differentiates between these two words in particular ways. Complicated things are not simple but knowable. For instance, not everyone knows how to fly a plane or how a car works. But some experts like pilots and mechanics do. Software is similar because not everyone knows how to write code, but some experts, like software engineers, do. So, software is complicated.
Complex things are also not simple, but they are never fully knowable due to the almost infinite number of variables in their environments. Complex things also have a very special characteristic known as emergent behaviour.
Any system involving people is complex, such as hospitals and businesses. While you can predict how those systems may behave, you can never be entirely sure like you can with complicated systems. For example, if the code conforms to the language's syntax and follows the correct process, it will compile and execute. In contrast, complex systems may behave differently compared to when they are observed knowingly or secretly.
Modern code is complicated
Now, where this gets tricky is software systems. Most software systems are so big that no person can ever fully know how everything works and fits together. Additionally, technology is moving so fast that it's getting cheaper, faster and simpler looking to do more with less than ever before. Just look at all the cloud technologies or all the different software packages available with your programming language of choice. You don't even have to understand how they work. You can just start using them. All this makes using other people's code and tools vastly easier. It often reminds me of this XKDC dependency comic strip.
This illustrates that modern code is often built on top of other people's code. But all that complication is abstracted away by neat little interfaces. Resulting in foundations of systems that we don't even know we're built on. All of this results in highly complicated software systems. Therefore, no one could ever fully understand how the system works or fits together.
Modern code is built and maintained by teams
With no one person being able to know everything and handle everything with our software systems leads us to work in multidisciplinary teams with the hope that all these different disciplines will understand the system. This results in our team members becoming interdependent and needing the support of each other to build and maintain their part of the system.
However, multiple teams often build modern software systems, each responsible for different parts. Think of frontend teams handling the system's UI, e.g., mobile apps or websites, with backend teams running the web services that provide the content for frontend teams to display to users via APIs. This can result in those teams becoming interdependent. For instance, the frontend teams have nothing to display if the backend teams don't make that content available to them. Backend teams have no way to surface their content without frontend teams etc.
Geometric complication of team communication
But this is where things can get even more complicated. The more people and teams we add, the more geometrically complicated the lines of communication become.
Therefore, understanding who knows what in the teams becomes harder and harder the more people and teams we add. We just have to trust that someone does.
Software systems are complex
Coupling the complicated code and the numbers of people and teams has an almost compounding effect on the amount of complication within modern software systems.
This results in software systems having many variables in their environment that are hard to see and understand and exhibit emergent behaviours, i.e. the quality of the system. Again, from What do Quality Engineers do?
One aspect of quality we often miss is that quality is an emergent system behaviour. You can not inspect the individual sub-systems and deduce the overall quality attributes of the more extensive system. You have to take a holistic view of the sub-system components and their interactions to understand how the system behaves and, therefore, how its quality attributes are likely to manifest.
Most software systems are much more complex than most people realise. So modern software systems are often complex, not complicated.
What is complicated and complexity?
This is where I like to use Maikel Mardjan's simple definition from NO Complexity blog:
Complicated = not simple, but ultimately knowable
Complex = not simple and never fully knowable
Why does this matter? So we can nudge and boost our teams
From What do Quality Engineers do?
By applying a Quality Engineering mindset to make the system healthier, we can nudge and boost aspects to produce the quality attributes we want, which can be the seeds that grow into a culture of quality throughout an organisation.
Having a common definition of what we mean when we say something is complicated and complex and being explicit that we see these things as different allows us to start having better conversations within our software teams about what is fully knowable and what is not.
A code module could be considered complicated and, therefore, fully knowable in its behaviour. If this is true, then we should be able to fit code-level tests around said module. Then, whenever we change the module's implementation code, we should be able to run all those tests. If they pass, we can say with relatively high certainty that the module's behaviour has not changed.
Suppose the module is subsequently integrated into a larger system, but the overall system contains code from external sources. In that case, we can consider it a complex system with potentially unknown behaviour. Suppose the only change that has gone into that complex system is our changed module. Then, the team's risk threshold may not have been exceeded, and the system could be pushed to production. But suppose modules from external sources have been updated. In that case, this may breach our risk threshold, and we may want to do more due diligence before pushing to production. For example, doing more exploratory testing, running additional automated end-to-end tests, or executing our retro-fitted tests around the externally sourced modules.
By differentiating between complicated and complex systems, we can signal that while our systems are not simple, there have known and unknown behaviours. Depending on where changes have been made, it may warrant more scrutiny. But also spark conversation around how to move more of our complex systems into the complicated hemisphere.
In this way, we nudge the teams to use more explicit language and then boost their approach to building quality by helping them think of ways to make their software systems more predictable in their behaviour when changes are introduced.
Further reading
Complex versus Complicated – NO Complexity
Developing Leaders: 6 Keys to Identify & Develop New Leaders - Lighthouse Blog
A Leader's Framework for Decision Making - HBR, 2007
Nudging and Boosting: Steering or Empowering Good Decisions - Ralph Hertwig, Till Grüne-Yanoff, 2017