OOMP: Rewriting Code

By | July 20, 2026

Complete Rewrite

Many times, when (one or more) developer(s) encounter code for the first time, the immediate thought is to rewrite it. This is also a not-unusual reaction from a new manager/director/etc. who becomes responsible for a new system.

A complete, from-the-ground rewrite of a system is a horrible idea. A running software system is not just a solution to an easy-to-explain problem. Often a system has evolved over time to deal with many issues that weren’t considered initially. Other times changes in business, technology, or legal issues forced changes in implementation.

Many times changes to the original idea are considered as warts, flaws, or technical debt. While those terms might be appropriate. A better term is probably scar tissue. These are places where the original pristine design and implementation ran into the harsh reality of the real (potentially unspoken) and possibly changing requirements. Scar tissue isn’t a flaw, it’s the result of a lesson. Just replacing all of these lessons with a new, clean system usually means re-learning all of these lessons all over again.

If the system is being rewritten because it is successful or critical, learning these lessons will probably be more painful than it was when the lessons were originally learned. The system was probably not under as much stress originally, and may not have been as critical. What might have been a minor issue when the customer base was smaller could be a fatal mistake with more or bigger customers.

Small Improvements

Often a better approach than a full rewrite is a series of small improvements and refactoring to improve the worst problems with the code. This approach appears slower than than a big bang rewrite, but is less risky and doesn’t cause uncontrolled time spent fixing issues that you didn’t realize existed. This also doesn’t mean a refactoring sprint which some teams use to make a substantial rewrite seem like clean-up. We are talking about small changes, like the ones described in Refactoring by Martin Fowler.

Improving naming (to match current understanding), intent-revealing method extraction, documenting metaphors and important terms, along with a host of other changes can convert a hard-to-understand code base into one that is pleasant to work with. Part of that change is improving the code, the other part is improving the understanding of the code.

Small improvements can add up. If you can make small improvements with every change to the code (and only on the code you are already changing), the code begins to look better. Moreover, other people will see the improved code and make similar changes. People are often surprised how quickly it makes a difference.

Know Why

When you remove cruft, make sure you know why it is there. This is an example of Chesterton’s Fence. The general principle being that until you know why something you don’t understand exists, you may not be able to determine if you are right in wanting to change it.

Some examples that I have seen in my career:

  • Odd code rules that represent legal requirements
  • Odd business rules that apply to a particular customer
  • Contractual requirements by a customer
  • Strange code needed to work around a problem in a critical library
  • Code to deal with a particular floating point math bug in the CPU
  • Performance optimizations

Once you understand the reasoning behind something odd, you may decide that it really is still needed (in which case, a little documentation is in order). On the other hand, you may realize that it is no longer needed and can be removed. Many times you may learn that the original change is no longer correct and needs to be modified. But, you always need to be aware that just because you don’t understand something, doesn’t mean it isn’t still needed.

Theoretical Improvements

Sometimes people will identify something is theoretically wrong in a system, even though it is not causing any visible issues. Sometimes what is identified is not wrong, it can just be improved in theory, with a change.

Don’t break a working system for a theoretical improvement. These kinds of changes are more insidious than other mistakes you can make modifying code. Sometimes, the current implementation may be less theoretically correct, but handles limitations that occur in the actual environment than the theoretical case would have. A good example is the order of floating point mathematical operations. When working with changes over a large dynamic range, the order of operations can result in quite different results.

Sometimes a theoretical improvement can help. But, it is easy to forget that reality beats theory every time. If you are making a change for theoretical reasons, be sure to verify performance, edge cases, boundary conditions, race conditions, and anything else that might have been ignored by the theory.

A great example of this is creating a hash from an array to look up a key once. The key lookup on a cache is O(1), so this should be great. But, creating the hash is O(n), so you spend more time creating the hash than you ever recover using the hash. In order to do this, you need to understand how many times the hash will be used relative to the number of times it is created.

Another example is using caching without an understanding of cache-lookup overhead versus re-calculating the cached value. Often caching helps, sometimes it’s much worse.

Getting Worse to Get Better

Sometimes you have to make it worse to get better. This is often a frustration for someone who hasn’t slowly improved a code base.

As an example, if there is an abstraction in the code that no longer matches the understanding of the developers, it is a hindrance to work. correcting this kind of problem can result in added duplication in the code as you unravel the abstraction. Moving functionality to a newer approach may result in confusing method names as the work goes on. As the team or developer settles on the new abstraction, the code improves again.

For risk mitigation, these kinds of changes may appear over several changes in an attempt to avoid breaking production. This is good because smaller changes are less risky, but bad because the code looks worse for a while. It requires a bit of discipline to recognize that the changes that make things worse are temporary and trust that the end result will be better.

Pick Your Battles

If a piece of code is ugly, unreadable, no one know how it works, but it doesn’t fail, leave it alone.

This is a really difficult lesson to learn. Old, ugly code that works is better than new, clean code that doesn’t. We almost always have more work to do than time to do the work. Focus on improving the code that we are having to change anyway. If you don’t already need to change the code (because it works), improving the design is less important that improving design where it will reduce the work you already need to do.

The goal of improving the design is not primarily to make the code pretty. The primary goal is to make the work we need to do easier and safer while reducing problems. If we will not be changing an older, uglier piece of code for other reasons, cleanup is not a priority. This is a hard lesson to swallow.

Maybe Still Needed

Sometimes (less often than you think), a complete rewrite is still the right thing to do. If we need to go this route, be prepared for it to be harder, take longer, and be riskier than predicted. If possible, re-writing in place (basically a larger version of the refactoring from above) is probably the most likely to work. It also has the advantage that a partial solution still leaves you better off than a partial new system that can’t replace the original.

If you can’t rewrite in place, replacing parts of the system and transitioning traffic to those parts is the next lowest in risk.

These might seem overly risk-averse, but the normal case for a re-write is to start a brand new system that is going to exactly match the current functionality but with a better design (newer paradigm, different language, etc.). After a little while, the requirements begin to drift. Maybe

  • the business wants new functionality that is implemented in the old system
  • the business decides on new functionality in the new system
  • developers learn more about the problem space and discover more work than expected
  • difficulty with dependent systems contact with the new system
  • difficulty with data compatibility between old system and new

Most likely the problem will be some combination of the above, and that combination may drift over time. I’ve never seen a major system re-write take less than a year. Many go more than a year then are abandoned, because it doesn’t have feature-parity with the original system. Sometimes the new system is put into use, but the old system remains to deal with feature that have not yet been implemented.

Leave a Reply

Your email address will not be published. Required fields are marked *