Systems require maintenance to ensure continued operation. Consider mechanical systems as an example. These systems are subject to wear and tear and require maintenance strategies that lay out how to deal with issues such as those. The strategies can be preventative, like regular refilling of lubricating oil to limit the extent of frictional wear. They also can be corrective, like the replacement of worn-out or broken parts as soon as they start causing problems. The essence of this can also be applied to software.
Software, unlike physical machines, is not subject to wear and tear. This is especially true in this age of cloud computing where software providers don’t have to worry about the maintenance (think cooling, replacement of faulting drives, etc.) of the physical servers on which their systems get deployed to execute. With software, the equivalent of wear and tear is what is called software entropy: as you introduce changes into a system, the system only gains disorder. The maintainability of a system, that is how easy it is to change, is indicative of the level of entropy in the system. The easier it is to make a change, the lower the entropy. Conversely, a high entropy system is difficult to change. Consequently, much of software maintenance is concerned with the strategies that try to limit the extent of disorder introduced by making changes to the system.
In this article I present good design as the first preventative step in the maintenance of software, since without it, a system is most likely doomed from the start. I also highlight some principles that can be followed to keep software amenable to change. I end with examples of software maintenance in practice.
Setting up a system to be maintainable:
Elements of good design:
One of the hallmarks of well-designed software systems is their maintainability. That means if down the line, a change must be made to the code base, that can be done easily, without causing unintended side effects elsewhere in the system, and without complicating the code base too much. Systems evolve, and change is inevitable. Change can take many forms including addition of new features, or even retirement of unused features. The best approach to make maintenance pain-free is to follow design patterns and principles that promote loose coupling and high modularity. To make a system easy to maintain, the architecture should be designed with the concept of the inevitability of change in mind and with the goal of making that easy. Lower layers should also be designed following sound principles, keeping the inevitability of change in mind.
Design patterns:
Design patterns represent documented wisdom accumulated by experienced software developers over many years regarding the effective organization of software systems to limit entropy. They provide established solutions to recurring design problems. By applying design patterns, developers create highly cohesive modules with minimal coupling, promoting better organization and easier maintenance of code. Additionally, design patterns encourage code reusability, facilitate easier program changes, and allow for logical expansion as a product evolves. One example of a good pattern is the atomic design pattern that is used to make maintainable frontends.
Atomic design: an example of a good pattern for frontend development:
With atomic design, component systems are crafted using five basic building blocks: atoms, molecules, organisms, templates, and pages. When applying atomic design to front-end development, you start by creating small, reusable components (atoms) and gradually combine them into more complex components (molecules, organisms). This approach promotes modularity, maintainability, and scalability in your codebase. Atomic design’s modular approach facilitates the creation of reusable components. When changes are made to an atomic component (like an atom or molecule), those changes propagate to all instances where it is used. This consistency ensures that updates are applied uniformly across the interface, making maintenance easier.
Readability:
Developers should write code with the goal of making it such that other developers can independently understand what it’s doing. To this end, readability is important. I believe it is a duty owed to fellow developers (and your future self) to write readable code. This includes things like choosing good names for variables and methods, that are descriptive and informative so that in a literal sense the code is the documentation. Another way to make code readable is to apply the single responsibility principle, which in practice is achieved, for example, by limiting the scope of responsibility of a function to one thing. If a function does a lot of things, as indicated by having too many lines of statements within, then it becomes beneficial to isolate sets of statements into smaller functions and invoke those functions within the first. In addition to decluttering the first function, the inclusion of isolated functions with limited functionality improves the readability of the code substantially, given that encapsulation in a function creates the need for a name that captures the intended outcome of the steps/statements within that new function. Later it will be easy to know where to make the changes as the code will be self-documenting.
Unit tests as a tool for maintenance:
One of the ways to make the code base amenable to change is the inclusion of unit tests. In addition to verifying that a given implementation adheres to the intended logic and achieves the intended functionality, unit tests serve as guardrails to ensure future work on the code base does not inadvertently break the features that are already in place. This gives courage to the programmer to go in and make a change when necessary, knowing that regressions will likely be caught by the unit tests. If the passing of all unit tests is a requirement for successful deployment builds, then the developer must resolve all fails before checking in the code, however, they need not scrutinize other areas that are completely unaffected. Hence unit tests can be a great aid towards maintenance.
Maintenance in practice:
Code Refactoring:
When under pressure to deliver a time-sensitive solution, a simple implementation that gets the job done may be ideal. However, it is wise to come back later to disentangle the code base and to make the system more robust. This is code refactoring. It involves restructuring existing code without altering its external behavior. There are several reasons to refactor code: to enhance readability, ensure consistent coding practices, create reusable components, detect hidden bugs, improve maintainability, prepare for scalability, and optimize performance. Refactoring tasks include the following:
- grouping related code into smaller, well-named methods for clarity and maintainability,
- removing temporary variables and directly using expressions,
- providing methods to read/write data instead of direct access,
- simplifying methods with obvious logic,
- cleaning up unnecessary code,
- and using descriptive names for better readability.
Monitoring and optimization:
Business is concerned with whether the system is effective in advancing their goals. One way this is monitored is by using analytics. Put in place analytics tools that monitor the system across metrics such as performance and generate reports. Tools for the web such as Google Analytics and Microsoft Clarity generate information about user interaction and performance. They provide information on visitor demographics, traffic sources, user behavior metrics, conversion rates, content performance, e-commerce data, page load times, and A/B testing results. Insights from these can lead to ideas about how the system can be improved to enhance its effectiveness. These may lead to requests to retire a feature or a part of it, to slightly modify the behavior, or to add a temporary feature to meet seasonal promotional needs, and so on. These are inevitable and it helps a lot if they can be met in a timely fashion. This can be ensured to a great extent by following the above recommendations around design.
Bugs:
No programmer sets out to create bugs. Nonetheless, bugs do sometimes make it into the code to be discovered by the end users. Ideally, these should never make it past QA testing, but sometimes they make it to production. The resolution of bugs forms part of maintenance. If the recommendations from above relating to design were followed, then the investigation to discover what is causing the bug should be a short one and the change to fix the bug should be a quick one, thereby saving you the potential negative implications of delaying the fix, such as fractured user experience and financial losses. Measures should be put in place to allow easy reporting of bugs that get discovered at different deployment stages up to and including production.
Dependencies:
Regularly reviewing and updating your dependencies is crucial for maintaining a healthy and secure project. Keeping dependencies up to date ensures you receive security patches and fixes, reducing the risk of vulnerabilities. Updates also include stability improvements, performance enhancements, and compatibility with other libraries. Dependencies adapt to evolving standards, language features, and industry practices, so staying updated keeps your project relevant. Ignoring updates accumulates technical debt, making maintenance harder over time. Active communities provide ongoing support and documentation improvements. Regular updates help you comply with licensing requirements and avoid legal risks. Additionally, new versions optimize code and enhance performance.