Complexity in software development

#architecture #complexity

Written by Anders Marzi Tornblad

Introduction

In software development, we encounter a lot of challenges that can affect the efficiency and quality of our work. Throughout my career in software, I've observed some recurring themes that often lead to unnecessary complexity, over-engineering, and technical debt in large software systems. While it's tempting to consider these issues in isolation, they're really interconnected, and their combined effects not only add together, but can be multiplicative. Here are some of my insights from 25+ years as a software person.

Lack of refactoring

Refactoring is an integral part of software development, yet it's too often overlooked. In short, it involves restructuring existing code without changing its external behavior. The absence of regular refactoring leads to a gradual increase in technical debt, a concept that reflects the extra development work arising from choosing an easy, limited solution right now, instead of using a better approach that would take longer.

Just over 10 years ago, I was invited to carry out an architectural review of a system for a team in the automotive industry, that struggled with slow development speed and low software quality. The team had avoided refactoring, in part due to time constraints, but also because they were not used to modern Agile principles in software development. Over time, the codebase became so spaghettified that adding new features or fixing bugs became almost impossible.

Premature generalization

Donald Knuth famously said "Premature optimization is the root of all evil." In my experience, this is still true, but I find premature generalization even worse. Developers, sometimes make code generalized and configurable, eager to impress or out of a misplaced sense of "correctness", long before it is really known that the code needs to be generic, because "best practices" say so. This leads to complex, hard-to-maintain code, where the Shotgun Surgery antipattern drags productivity down to a complete halt.

It is always important to find a balance between generalization and readability. I can't count the number of projects I have been involved in where developers had introduces interfaces and factory patterns and configurability, but only one single concrete implementation was ever used. This inevitably led to a codebase that was very flexible, but nearly impossible for new team members to understand.

Pressure to deliver quickly

Software development's primary goal is to deliver business value. In a competitive world, time is often a make-or-break factor. However, the pressure to deliver quickly can lead to cutting corners. Quick fixes might seem like a good idea under time constraints, but they result in growing technical debt. I've seen projects where the push to release early led to significant post-launch issues that could have been avoided with a more measured approach. Then crashes and performance issues are often blamed on *"the software team"*, and the responsibility to fix the issues is seen as a cost item in the books.

Insufficient documentation and knowledge sharing

The importance of documentation and knowledge sharing cannot be overstated. They are one of the pillars of effective team collaboration, especially in complex business environments. When knowledge is siloed or documentation is outdated, the entire team's productivity suffers. New members struggle to get up to speed, and existing members waste time deciphering the codebase. Collaborative tools and practices are essential in fostering an environment where knowledge flows freely.

Weaving it all together

The interplay between all of these aspects can create a challenging environment. For example, the pressure to deliver quickly often leads to a lack of testing and insufficient documentation. Similarly, premature optimization can make future refactoring efforts more challenging, increasing technical debt.

As a software architect, my role is not just about designing systems but also about nurturing the processes and practices that lead to sustainable and maintainable software development. It involves advocating for time to refactor, resisting the urge to generalize and optimize prematurely, and ensuring that documentation and knowledge sharing are integral parts of the development process.

Conclusion

The path to efficient and effective software development is not linear. It requires a continuous balance and reassessment of our practices. As someone who has navigated these waters for a quarter of a century, I've learned that the key is not just in understanding these challenges but in actively working to integrate solutions into every phase of the software development lifecycle. By doing so, we can build not just software, but also teams and processes that are resilient, adaptable, and ultimately more successful.