The call for simplicity
Software engineering as decision making
As software engineers we are called to make decisions. I would go as far as saying that most of our work revolves around it. From naming to choosing technologies, from picking the most appropriate conventions to outlining the best team process. Decision making is thus so deeply and subtly rooted in our craft that we often forget about its implications. What I want to explore today is the ability to settle for the lower end of the spectrum, i.e. choosing the simplest option and why you should give it more than a fleeting thought.
Simplicity is not naïveté
When I started as a software engineer, the world surely appeared more like a black and white panorama to me. I knew certain problems required specific solutions and I was not willing to compromise. To exemplify, I would like to tell you a story.
When I started working at Mind Foundry, I went through an interview process as it’s expected from all candidates. I flew to Oxford specifically to take that one test and I really hoped to make a good impression. The first exercise was a coding test. Unfortunately, I cannot remember the finer details of the problem I was called to solve, but I do remember my solution was artsy and quite pretentious: it used a trie, a niche prefix search tree. While I was proud I chose the perfect data structure for the job, I was internally panicking and hoping my implementation would work as expected. As many of the readers probably know, graph algorithms are cool but require a great deal of precision in order for them to function correctly: being under pressure during a job interview is not exactly the ideal coding scenario. To my surprise, however, the toy program worked as expected even for edge cases. I could breathe again! Then one of the interviewers asked me a simple question: “Why didn’t you use a set instead?”. A set is a simple data structure every developer learns in their very first years. I was taken aback. I convinced myself my choice provided the right tool for the job, despite its taxing mental requirement. I wanted to show off and chased a fitting yet complicated solution.
Thinking back, after several years at Mind Foundry, I realize why I was asked that question. Sometimes, going for the simpler solution allows us to focus on other problems. While it may not be optimal, it reduces mental burden, speeds up development, makes it easier for other people to understand our thought process and enables future expansions. These are the key takeaways I want to focus on in the next paragraphs.
Less mental burden
While it may not seem intuitive at first, mental burden can be a problem for developers. This may be counterintuitive: after all, developers are expected to perform intellectually taxing endeavors - to translate natural language into code. However, there are many issues that require attention: user experience, correctness, data validation, handling of errors and edge cases, etc… These are merely examples of a plethora of concerns that need to be juggled together at all times. That’s why going for a simpler solution may be beneficial for velocity and development efficiency. It frees up some brain power that can be addressed to other potential problems. Beware this is not different from any other form of decision making. A simpler solution will likely have other shortcomings. For example, to show up-to-date information in the UI, one may decide to opt for a short-polling approach: the frontend will keep querying a remote server for data every few seconds. This is wasteful but easy to implement. It may lead to performance issues but any developer, even the most junior, can understand what the code does and build on it. Whereas streaming solutions or push requests may solve the same problem better, they require more expertise and involvement; and a platform may not be mature enough to justify these choices. Tradeoffs are inevitable.
Working in a team requires certain coordination. Meeting deadlines is important to uphold trust between the company and its customers. That’s why it’s sometimes advisable to choose a faster route. This comes with shortcomings, as with everything. The price we pay for a faster delivery today is tech debt: compromises that will need to be addressed in the future to lower the toll of our past decisions. Many developers are afraid to make compromises but tech debt should not be treated as a scarecrow to keep at an arm’s length all the time. Instead, it’s part of the development process. We should always consider it as a variable and work towards reducing it whenever the external pressure lessens: in between releases, during slack time or when requirements change.
For example, it may be valuable to foster this way of thinking during the greenfield phase of a new project. I personally had such a discussion recently, whereas my team was considering introducing a publisher-subscriber model to implement a task queue distribution system. However, the setup and training costs would have required us to delay the MVP by a considerable amount. We opted for a simple polling solution, which serves as a stopgap for the time being - with the caveat that we would introduce the more complex solution when necessary.
The more a software engineer works in a team, the more they get to appreciate understandable code. Now, the matter of clean and understandable code can lead to all sorts of discussions, which can be expanded upon by citing an array of books. Here I only want to shift your attention on how simplicity can foster code cleanliness. Many professionals have been in a situation where a more complicated solution seemed ideal, from the point of view of craftiness, performance or correctness. We still need to consider how our contributions are received by colleagues and team members of various experience levels. For example, it may be tempting to be clever with the innards of your technology of choice, but if that prevents other developers from understanding what you are doing then it defies its purpose.
Start small, delay complications
In the past years of working at Mind Foundry, I learnt a valuable lesson. We need to watch out for overengineering. It may be alluring. Sometimes, though, it is better to show humbleness and restraint in going for the seemingly simpler “dumber” solution first; and only when things have settled and the system is working properly, then improve what was built earlier. Starting with a smaller architectural piece also enables a keener understanding of how it interacts with the other many moving parts of a complex system.
Everything I wrote so far can be condensed in a self-contained wisdom which many will find remarkably familiar:
- Make it work.
- Make it right.
- Make it fast.