Why Rewriting Is Essential for Keeping Software Alive
Rewrite software components regularly to avoid rewriting all of it eventually
The software industry has changed tremendously over the past decade. One of the things that’s changed the most is the software development process.
That change was triggered by an ongoing increase in the rate at which new user requirements have to be implemented, packaged, and shipped. Instead of waiting a year or more for the next software release, software manufacturers today have monthly, weekly, and even daily (or nightly) releases.
In the past, waterfall was the prevalent software development life cycle (SDLC) model. This process had only one cycle of design and development, after which the software entered a long maintenance stage until it died, making “death” an inherent stage in the process.
As the software industry matured, software companies moved towards the incremental/prototyping model, in which the software is designed and developed incrementally, adding more features each time.
This process evolved further with the practice of continuous integration and delivery (CI/CD), to the point where the line between development and maintenance has completely blurred. Software moved from being inanimate to being dynamic.
The software of today is like a living organism. It requires proper nurturing in order to sustain life, and for that, it must maintain a healthy diet. That diet, in my opinion, should be based on one important nutrient: rewrites.
Think of software development as drawing in pencil. If you need to change something you’ve previously drawn, you erase it and draw over it. However, doing so never completely removes the previous pencil marks. There are always some leftovers.
In the art world, this phenomenon is referred to as hesitation marks. These marks are visual evidence of the artist’s thought process and are considered by some to be aesthetically pleasing. However, even the most eager proponents of this phenomenon would agree that there’s a limit to what the paper can suffer. There comes a point when you have to start over.
While hesitation marks may be considered an aesthetic pro in the art world, the software development world strives for a more polished appearance.
The equivalent of hesitation marks in the software world is messy, unreadable code (aka spaghetti code), which is often the result of a change in the requirements, just like erasure marks are the result of the artist changing their mind.
The descent into spaghetti-code chaos begins when the customer decides that they want a new feature or a change to an existing one. The customer’s demands are translated into new requirements, and after going through review and design, the developers receive them and it’s time to dig into the code. This occurs very often, and just like in pencil drawing, soon enough the code stops being aesthetically pleasing and becomes one big mess.
Working with messy code is not much fun. You have to spend a lot of time trying to read and understand it, but you never really do. When you finally decide to touch the code and change something, things tend to break unexpectedly and terribly. The code is no longer maintainable. So where did we go wrong?
When developers receive new requirements that diverge from the original set of requirements and cannot be easily supported by the current code, they have two options:
- Refactor the component. Force the code to work with the new requirements by applying a workaround. This is the “quick and dirty” solution.
- Rewrite the component. Write the code from the ground up based on the new requirements and the lessons learned from the old code. This is the “slow and clean” solution.
Both developers and management will often lean towards the first option. Management sees it as the cheaper and faster option, while developers see it as the easier option (and I don’t blame them).
While the first option may be quicker and cheaper in the short term, it is far more expensive, more time consuming, and more complex in the long term. If we expect our software to live long, short-term benefits should rarely be favored over long-term ones. If you’re in it for the long game — you should go for the second option. Here’s why:
- Code is a reflection of its requirements. If the set of requirements is rewritten, the code must be rewritten to reflect that.
- Workarounds and partial solutions only delay the problem, they don’t solve it. Eventually, you’ll end up spending time and money on both the workaround and the rewrite.
- Delayed problems tend to grow the more they’re delayed. When you finally realize that you have to rewrite, the effort is much greater than it would have been had you done it before.
- Workarounds make the code harder to read and modify. Unreadable code leads to fragility and increases the chance of introducing new defects.
- Unreadable code also hurts the productivity and motivation of the developers who have to work with it. In extreme cases, it may cause developers to leave a project.
The fastest way to write code is slowly, according to “Clean Code: A Handbook of Agile Software Craftsmanship” by Robert C. Martin. Therefore, “fast” refactors are slow, and “slow” rewrites are actually fast.
When to Rewrite
There’s a balance to maintain between refactors and rewrites due to their trade-offs in terms of duration and price. You should be prudent when choosing one over the other. The following traits are a good indication that your component is due for a rewrite:
- When the code becomes unreadable. This happens after going through several refactors, or perhaps it was poorly written to begin with. Another refactor will only make things worse.
- When the new requirements are diverging too far. The code has been written based on certain assumptions, and now with the new requirements, these assumptions are no longer true. A refactor is not enough in this case.
- When new information opens the path to a better design. Sometimes new requirements give a clearer picture than was available before. This, combined with the lessons learned from the previous implementation, may bring forth an improved solution.
- When new technologies become available. It’s important to stay updated with the latest technologies. This includes using the latest features of your programming language, keeping your third-party packages up to date, and keeping up with the latest trends. It’s important to keep your code fresh, otherwise, it will slowly decay into an unusable state.
Rewrite software components regularly to avoid rewriting all of it eventually.
How to Rewrite
There are two types of rewrites: internal and external.
An internal rewrite changes the internal structure of the component without affecting the component’s exposed API, leaving its dependents unaware of the change.
An external rewrite may or may not change the internal structure of the component, but it will change the component’s exposed API. Such a rewrite affects the component’s dependents, and there are two ways to handle it.
Deprecation and Adoption
This is the method of choice when you have no control over the dependents of your component, or if they are too many to update at once.
In this approach, new component and old component coexist until all the dependents using the old component adopt the new component.
The old component receives a deprecation status that can be indicated by its name (i.e.
MyComponent will be renamed to
DEPRECATED_MyComponent, whatever your convention is) or in the documentation (for example, JSDoc supports the
This is the method of choice when you have full control over your component’s dependents and there aren’t too many of them to update at once.
In this approach, the new component replaces the old component and all the dependents are refactored to use the new one without a deprecation process.
I tried to focus on the case for rewrites since, in my experience, they’re underrated and feared when compared to refactors. Even worse, rewriting is often viewed as an admission of failure by the development team, making them guilty for being unable to write flexible code and predict future requirements.
But just as it is impossible for product owners/managers to predict every possible future requirement, it is impossible (and even damaging) for the developers to write their code that way.
That doesn’t mean that there’s no case for refactoring. But the delicate balance between refactoring and rewrites is better maintained when rewrites are equally considered.