Complexity and software development
Complexity and software development always go hand in hand. As a piece of software evolves, it also becomes more complicated, accumulates subtle (and not so subtle) dependencies, and, if we’re not careful, it can also accumulate complexity and become unnecessarily hard to understand and modify. Complexity can’t be eliminated, but it’s our job as software engineers to try to minimize it as much as possible.
Some of the symptoms of complexity that we need to watch for include:
- Shotgun surgery: simple changes that require making modifications in several different places.
- High cognitive load: the need for a developer to know an enormous amount of information about the system to complete a task.
- Unknown unknowns and obscurity: when it’s not obvious what parts of a system need to be modified, or what information the developer needs to know to complete a task. This can also refer to when the system has obscure or unknown dependencies or when documentation is misleading or nonexistent.
My team and I encountered a few examples of these three symptoms recently while working on a new feature for the Church Center directory.
The Church Center directory is a complex product, available as both a web application and a mobile application, which means that there should always be parity between them when developing new features. Settings are controlled by administrators through the People web application but the directory only accesses them through API requests to Publishing. The People application, however, doesn’t have direct access to modify the settings object and must make an API request to Accounts for modifications.
Because of the way the Church Center directory depends on all these other products, implementing a new feature meant making changes in all of these different places. Many times, it wasn’t clear what needed to be updated or where since not much documentation existed about it, so we resorted to reading the codebase and following clues from previous pull requests that made similar modifications. This is unfortunately a good example of all three symptoms of complexity combined.
An opportunity to deal with even more complexity arose when it came time to roll out the feature. We had decided on a date when the feature would be available for everyone but wanted to provide an opportunity for some customers to start using it earlier, play with the defaults, and decide if they liked those defaults or wanted to disable them.
Usually, we resort to using feature flags, such as Flipper, for slow rollouts, but coordinating feature flags between several products can be daunting and prone to error. Additionally, we required users to opt themselves in, requiring them to enable their feature flags wherever necessary, and that wasn’t an easy thing to do, since feature flags would need to be set in at least three or four different products. Certainly, trying to accomplish this feat would add to the complexity of the system as a whole.
So, instead of adding more complexity, we decided to simplify it by relying on the one source of truth that is accessible to all products involved: the settings object. We added a new attribute to the settings object that would record the date an organization opted in. This attribute would be managed through the People web application and accessible to the directory (both web and mobile) through the same API request it already makes to access other attributes in this object.
While it’s true that this new attribute is meant to be temporary and that it will be deprecated and removed during some future cleanup process, I still consider that this option was a lot simpler than trying to orchestrate multiple feature flags across different products.
There are some useful takeaways from this experience, the most important one being that we should always keep our eyes open for opportunities to simplify complexity. For me, another important lesson was about the importance of documentation. As software engineers we don’t usually write enough documentation. We often assume that our code, if written correctly, will document itself. The truth is that documentation will always enhance a good design by revealing intention and providing clarity in ways the code itself cannot.
If you wish to read more about some interesting takes on complexity in software design and ways to reduce it, I strongly recommend you read A Philosophy of Software Design, by John Ousterhout. A word of warning: after reading this book you may never look at software design the same way again!