Software Engineering as Decision-Making
(This post was originally featured on the Mind Foundry website)
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.
Before getting hired at Mind Foundry, I had to go through a comprehensive interview process that’s designed to make sure everybody on the team is the right fit. I flew out to their headquarters in Oxford to begin this process, 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 happy that I had chosen 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 the 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, the mental burden can be a problem for developers. This may be counterintuitive: after all, developers are expected to perform intellectually taxing endeavours — 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 brain power that can be addressed to handle other potential problems. Choosing where we channel deeper intellectual focus is yet another decision we are called to make, and it is ultimately just another 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 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. As with everything, shortcomings are inevitable. One of the prices we pay for a faster delivery today could be 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 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 a quiet period, or when requirements change.
For example, it may be valuable to foster this way of thinking during the greenfield phase of a new project. Just a few days ago, my team was considering introducing a publisher-subscriber model to implement a task queue distribution system. But since 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 could serve as a stopgap for the time being — with the caveat that we would introduce the more complex solution when necessary.
At Mind Foundry, we pride ourselves on building AI solutions that are deeply explainable so that we can trust them in high-stakes applications. We take a similar approach to our code. 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
I’ve been at Mind Foundry for more than three years now, and in that time I’ve learnt many valuable lessons, both personally and professionally. One of those is that 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 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.
To further condense everything I wrote so far in a famous self-contained acronym, I will cite one of my favourite design principles: KISS, or Keep It Small and Simple. Used as a compass and taken with a pinch of salt, this concise wisdom will guide you through the perils and complications of modern engineering.
Nicolò Andronio is a Full-Stack Senior Software Engineer. He’s involved in the design, maintenance and development of the technology stacks that power several MindFoundry products. From UI to user experience, from database optimization to software architecture, to large data storage, integration and deployment of ML code, as well as code quality, enforcing best practices and mentoring. He takes pride in knowing the whole system inside out and making it better every day. During his free time, he likes to indulge in his creative side by crafting worlds, characters and stories as a Dungeons & Dragons game master.