tech, developers, and the code underneath

issue 171· essay·

Rewrites: when they actually work

The received wisdom is never rewrite. The received wisdom is mostly right and has three real exceptions.

The canonical advice is that rewriting from scratch is the single worst strategic mistake a software company can make. That advice is twenty-five years old and it is mostly still correct.

It has exceptions, and knowing which situation you are in matters more than knowing the rule.

why rewrites usually fail#

The old system's behavior is undocumented and load-bearing. Every strange conditional in a decade-old codebase is a bug report somebody filed. You will not find them by reading the code, because the code does not say why. You will find them by shipping the rewrite and having those bugs re-reported.

The rewrite has no users, so it gets no feedback. The old system is being exercised by real traffic continuously. The new one is exercised by your test suite, which encodes what you think it should do.

Feature parity is a moving target. The old system keeps getting features, because the business does not stop. You are chasing a target that recedes, and the chase consumes the time you budgeted for the rewrite.

Nobody can justify continuing past month six. The rewrite has produced nothing users can see. The pressure to redirect the team to visible work is enormous and usually wins, leaving you with two systems.

The knowledge is in the people, not the code. And the people who knew have frequently left, which is often why the rewrite was proposed.

the three cases where it is right#

1. The platform is dead.

The framework is unmaintained, the language runtime is past end-of-life and getting CVEs, the vendor discontinued the product, the hardware is unobtainable.

This is not a preference — you are being forced. Rewrite, and be grateful you found out before the security incident.

2. The domain model is fundamentally wrong.

Not "the code is messy." The core abstractions do not correspond to reality, and every feature requires working around them.

The test: can you name a specific business capability that is impossible, not just awkward, in the current model? "We cannot support customers with more than one billing entity, and the fix requires changing what a customer is."

If you cannot name one, you have messy code, not a wrong model, and messy code is fixed by refactoring.

3. The system is small enough that the rewrite is short.

If the whole thing is three weeks of work, the calculus changes entirely. The risk of a three-week rewrite is bounded. Just do it.

The received wisdom is about large systems, and people apply it to small ones where it does not hold.

how to do it when you must#

Strangler fig, never big bang.

Put a routing layer in front of the old system. Move one endpoint at a time to the new implementation. Route traffic gradually. When everything has moved, delete the old system.

             ┌─→ new service (endpoints A, B)
client → router
             └─→ legacy monolith (everything else)

Properties this gives you:

  • Value ships continuously. Every migrated endpoint is a delivered improvement.
  • Risk is bounded per endpoint. If one goes wrong, route it back.
  • The project survives leadership changes, because it is producing visible progress the whole time.
  • You learn the old system's real behavior incrementally, at the point where you have to reimplement it.

Run both and compare. For a while, send traffic to both implementations, return the old one's response, and log the differences. This is the single most effective technique for discovering undocumented behavior, and it finds things no amount of code reading would have.

python
old = legacy.compute(req)
try:
    new = rewritten.compute(req)
    if new != old:
        log.warn("divergence", request=req, old=old, new=new)
except Exception as e:
    log.error("new path failed", error=e, request=req)
return old      # old is still authoritative

Run that for weeks. The divergence log is your actual specification.

Freeze the old system's features. If the business keeps adding to it, you will never catch up. This requires an explicit organizational decision and it is usually the hardest part.

Set a deletion date and mean it. The worst outcome is two systems forever. If the migration stalls at 70%, you have doubled your maintenance surface permanently.

the question to ask first#

Before any rewrite: what specifically will be better, and how will you know?

If the answer is "the code will be cleaner," that is not a business outcome and the project will lose its funding to something that is.

If the answer is "features in this area will take one week instead of three, and here are the last five that took three," you have a case.

The rewrites that succeed are the ones where somebody could state the payback in a sentence. The ones that fail are the ones motivated by taste, and taste is not wrong — it is just not fundable.

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →