Avoid Software Rot with Strategic Maintenance
Software applications (like homes, cars, and nearly everything else) need maintenance. Even when the software itself doesn’t change, the systems/devices it runs on and the larger software environment are always moving forward. This is called Software rot. The longer legacy software sits on the shelf, the harder it becomes to work on it. It may even stop working on its own one day. This surprises my clients frequently, so I wanted to talk a bit about why this happens and how to plan for the software maintenance effort.
The World Moves On
Let’s take a look at an example from literature:
Bifil that in that seson, on a day,
In Southwerk at the Tabard as I lay
Redy to wenden on my pilgrymage
To Caunterbury with ful devout corage,
At nyght was come into that hostelrye
Wel nyne and twenty in a compaignye
Of sondry folk, by aventure yfalle
In felaweshipe, and pilgrimes were they alle,
That toward Caunterbury wolden ryde.
That’s a bit opaque, isn’t it? It comes from the introduction to Chaucer’s Canturbury Tales in its original Middle English. You can compare it side-by-side with a contemporary translation here.
Even though this text hasn’t changed since it was written in the 1300s, it is hard to understand today because the rest of the world has kept changing.
How Software Rots
It’s the same with software: Your application may remain unchanged for many years, but the rest of the technology world keeps marching forward.
No software product is built in isolation. Someone else built an interpreter for the language used in your app. Someone else built the processor on which it runs. Someone else created the protocol it uses to send data over the internet. Those things are all constantly evolving for a plethora of different reasons. Often, the changes are simply about fixing bugs and security holes, but they can also involve significant new features.
For example, Apple’s operating system OSX Catalina dropped support for 32-bit applications[1]. “Software rot” (in this case) is when your application is 32-bit or uses a 32-bit component. It might be easy to upgrade, but it might not be.
Types of Software Rot
That’s one example of how things can start to fail over time. Here are a few different kinds of rot:
- Obsolesence: Some third-party functionality is disappearing or changing, and your application needs to be updated to accommodate the change.
- Upgrades: An upgrade to get a bug fix from a third-party library might pull in the next full release.
- Platform or hosting changes: Hosting companies like Heroku may deprecate old services, so you can’t expect your app to work forever.
- Institutional knowledge: If no one has worked on your app for years, there will be a ramp-in period as a new developer joins, even if nothing is wrong with the app.
- Security: No one wants to write insecure software, but we often find errors after the fact and make new releases with fixes.
Don’t Let Things Rust: Types of Maintenance
Most software maintenance falls into one of these categories:
- Updating Dependencies – Updating the frameworks, libraries, etc. that your software uses to their latest and most secure versions.
- Version Support – Updating your application to support the latest versions of the platforms it interacts with (i.e., upgrading from Windows 7 to 10 or supporting the latest iOS).
- Project-Specific Improvements – Other updates and tweaks you’d like to make to improve your user’s experience with the software.
This preventative maintenance, all taken together, will tame most kinds of software rot. There’s also a ton of value in the small updates and learning that you get from coming back to a project and working on it after the world has advanced another year.
Here are the things I focus on to help maintain the quality and sanity of a large code base over time as part of software maintenance strategy:
1. Scrub Your Requirements
When specifying a feature of an application, it’s tempting to jump to the implementation. It’s natural to think in terms of things we have seen in other applications and want to mimic them. Too quickly, we jump to the how–and forget the why.
When specifying features of an application, it is best to be clear about the why. Implementations change, or some new widgets come along.
If you have quantitative requirements, such as response time or frame rate, make sure that you have the means to measure them before you commit to them. Lacking well-defined goals or a means of measuring your progress toward those goals can hinder your progress and lead to over-engineering and more complexity to maintain.
2. Automate Tests and Run Them
If you are taking the time to verify that something works, ensure you have a strategy to keep it working. Start every project with an automated test suite for any application or code library you are releasing to the world.
Don’t just say you will get to it once your project gets off the ground. Commit to it right from the start, and get a test suite running and monitoring the health of your project from the get-go.
Over time, and as complexity grows, it can be difficult to keep integration and system level tests running and passing. Although it can be tedious to stand everything up and get all of the pieces working together, it is worth the effort.
We naturally like to maintain a small scope while we crank out a new feature and weave our way through the labyrinth of our code base. While it is great having tests to validate the nitty-gritty details, high-level tests are the real glue that holds it all together.
3. Trim the Fat
If pieces of your project are no longer necessary, they should be eliminated. Supporting old behaviors costs money and energy, and it allows the complexity of your application to grow.
Many times, we will over-engineer parts of our systems as defensive measures when we are trying to hone in on a solution. When you end up where you need to be, look back at the trail you have left. Remove the effects of getting sidetracked and fixing things that may have not really been part of the problem. Simple solutions are are not only easier to maintain, but easier to understand for future residents on your project.
4. Leave Breadcrumbs…
Accept that code, on its own, is not self-explanatory of its intent. Before anything else, your tests should clarify the why. The how is likely much more easily grok-able.
If you have to do something nasty to fix a problem, leave a comment explaining why. You may even want to leave an apology for the poor souls who have to run into your solution months or years later.
How Often to Do Preventive Maintenance
It’s a balance. You want to do maintenance frequently enough to not fall behind. Once your old version of a tool won’t run anymore, it becomes much harder to work on. However, if you stop doing upgrades and other maintenance too often, you’ll spend more time dealing with updates than building features for your users.
Here are a few things we watch for:
- Major framework version changes. For example, Ruby on Rails only supports the two most recent versions of their software. When they release version 6, you should feel “on notice” that you have limited time before you’ll be forced to upgrade from version 5.
- Operating system changes. A new version of OSX or iOS will often change many things, such as API changes or new privacy restrictions on things like location services. When Apple (or Microsoft, or whoever) releases a new beta, you should start to figure out when you’re going to at least test it out.
- New versions of compilers and other direct project tooling changes. Changes to compilers and tooling can break compatibility with other third-party libraries you may be using — often in ways that are hard to predict.
Whenever I see any of the above, I’d definitely start planning when I’m going to make updates. If you pay attention to those updates, you can avoid slow deterioration and loss of quality over time.
One thing I would not pay much attention to is version updates to random third-party libraries. A modern software project has so many small, third-party utilities that it’s not really feasible to track them by hand and evaluate each one.
In general, I’d recommend “annually” as a good maintenance cadence for just about everyone.
How to Budget for Software Maintenance Costs
There’s no hard and fast rule for what number to put in your budget for any given software update. But there are some factors we can look at:
First, I don’t feel like it usually makes sense to budget for less than two weeks, no matter the project size. Here’s how that time might get split up:
- A day or two just getting the developer’s machine set with a dev environment, getting tests running, etc.
- A day to evaluate what needs to be upgraded: major frameworks, critical third-party libraries, etc.
- Two to Five days to work through whatever upgrade processes apply.
Beyond that starting point, I’d look at the amount that’s been invested in the project so far. Take a small fraction of that; a few percent might be a good starting point. The bigger the codebase, the more dependencies there are to update and test, and the greater the chance that an update could cause problems somewhere else in the codebase.
Put it in the Plan
The what and how much are very hard to predict because, typically, you’re dealing with things that your team didn’t build and doesn’t know intimately. But that doesn’t make it less important. Get maintenance on the schedule, regularly.
You’re going to pay for maintenance eventually, whether it be corrective maintenance or preventative maintenance. Make sure it’s on your terms.
[1] Apple wanted to support 64-bit operation in order to allow applications to access more memory for things like better multimedia handling. This is a trade-off: They need to support both versions, or they break all 32-bit apps. For years, they did support both 32 and 64, and then for a while, 32-bit apps would pop up a warning dialog. Now, 32-bit apps have finally been killed off for good.
We’ll send our latest tips, learnings, and case studies from the Atomic braintrust on a monthly basis.