At some point in every software’s life, there comes a day, when it is time to end the maintenance. For some software this is done gracefully after many years of service and business case being not relevant anymore. Sadly, there are software that reaches the end of maintenance prematurely and for this complete rewrite is needed to satisfy the business case. The question is: How to avoid rewrites and make the software survive in ever-changing platform?
At the end, everything comes down to evolvability of the software. Requirements change, dependencies are deprecated, and hardware is replaced. Quality software can evolve through everchanging landscape, but evolvability is rarely tested during the quality assurance.
In this blog post, I will give practical examples, how teams can build evolvable software.
Fail fast, fail often. There are always lessons to be learned in failures. In software projects, decision at the beginning of the project are more expensive to change, than decision made later in the lifecycle. So, detecting and correcting failures in these early stage decisions, is life-critical to the project. How to prolong these early decision, is another form of art, and I will leave it to future posts.
Failures at early stage of the project can be vendor choices, nonexistent business case or nonscalable architecture design. To detect these failures, build software iteratively in small steps, evaluate & test key choices after every step, stress test the system, use 3rd party auditors, and don’t ignore the early warning signs. Signs not to ignore:
- Change requests take ages to implement
- Bugs are left in the system for long periods of time
- In every release, something already tested breaks
If these problems arise at early stages of the project, it is guaranteed, that these problems will fester throughout the project, and at some point, stop the evolution of the software.
I argue that detecting failures isn’t the hardest part. The hard part is admitting failure and taking necessary corrective actions. Culture that doesn’t encourage failing fast, breeds non-evolvable software, because failures are hidden with watermelon reporting, and corrective actions aren’t taken.
Mind the dependencies! Dependencies might kill software prematurely. Dependency can be other software, like user interface framework, or hardware. Problems arise, when dependency functionality changes, gets too expensive or goes out of maintenance. In many cases, we have no control over our dependencies, so it’s vital that software can evolve by changing or dropping the dependencies when needed. Do not commit to a single framework dependency, because the framework maintainer is never going to make the same commitment to your project.
Tips to avoid one sided marriage to a dependency:
- Build software independent of infrastructure
- Inject dependencies
- Keep levels of abstraction consistent, don’t pollute higher levels with implementation details
Ditch the monolith. At the start of the project, it might seem convenient to put the whole program into the same monolithic system. Later, during the maintenance phase, it will be realized that implementing new features takes longer and weekly releases are no longer happening. Soon features roll-outs will stop, because the cost of evolving the system is too high.
Separating different functionalities to independent microservices, helps to maintain the evolvability of the system. Microservices can be evolved independently, without changes to other services.
Of course, microservice architecture isn’t the optimal solution to all software projects. In many cases monolithic approach is valid choice. However, microservice mindset should be kept when building a monolith, because software might need to evolve from monolith to microservices later in the lifecycle.
Comprehensive tests. Above points are all more or less architectural problems. Finally let’s talk about the code. Code quality needs to be high to enable evolvability of the software. I will write another blog post about what high quality code is, but I wanted to raise one very important quality aspect related to evolvability: comprehensive tests.
When working with a codebase without comprehensive tests, every change made is followed with a question: “Does this break something else?”. And after introducing enough bugs to unrelated functionalities, it is cheaper to do a rewrite, rather than keep maintaining the monster. Great decoupling can make changing the system less stressful, but nothing beats well-structured unit and integration tests.
In summary, evolvable software equals to quality software. However, evolvability is rarely tested during quality assurance, so it is up to developers to make the software evolvable.