Every few months a client tells me their old system is "unmaintainable" and asks how fast we can rewrite it from scratch. The rewrite vs refactor question sounds technical, but it is really a risk and cash-flow question wearing a technical costume. Get it wrong and you either freeze feature delivery for two years or keep pouring maintenance budget into a system that fights you every sprint.

I have sat on both sides of this decision: as the engineer proposing the plan, and as the person called in months later to explain to an owner why the rewrite that was supposed to take four months is now at month fourteen with no launch date. The pattern is predictable enough that I default to one answer, and I want to walk through why, and the narrower set of conditions where I actually recommend the opposite.

My default answer, almost always, is refactor incrementally. Rewrite from scratch is the exception, not the standard move, and treating it as standard is how businesses burn six or seven figures in rupiah for a system that does the same thing the old one did, just newer.

Why the Rewrite Instinct Is Usually Wrong

The rewrite feels satisfying because it promises a clean slate. No more workarounds, no more code nobody understands, no more "we can't touch that module." But a full rewrite has three properties that rarely get named out loud before the project starts.

First, you are frozen. While the new system is being built, the old one still has to run the business, so you are paying two engineering costs at once: maintaining the legacy system and building its replacement. Feature requests from the business get told to wait for "the new system," which usually means six months becomes twelve.

Second, you lose accumulated business logic you did not know existed. Legacy code is full of small, ugly, undocumented fixes for edge cases: the customer who gets a special discount code, the branch that calculates tax slightly differently, the report format one finance manager insisted on in 2019. None of that lives in a spec. It lives in the running code. A rewrite team reimplements the clean version of the business logic and quietly drops the edge cases, which only surface as bugs after launch, in production, in front of customers.

Third, rewrites have a well-known failure shape I call the two-year rewrite death spiral. Month one to three: excitement, clean architecture diagrams, fast early progress on the easy parts. Month four to eight: the hard 20 percent (the edge cases, the integrations, the weird legacy behaviors) starts eating estimates. Month nine to fifteen: the business, tired of waiting, starts requesting new features be added to the legacy system anyway, so now there are two moving targets. Month sixteen onward: the new system's scope has grown to match a moving legacy system that also changed. Nobody agrees anymore on what "done" means, and the project either gets cancelled after enormous sunk cost, or ships in a rushed state that recreates most of the original problems within a year.

What I Recommend Instead: The Strangler Approach

The strangler pattern, named after strangler fig trees that grow around a host tree and gradually replace it, solves the freeze problem directly. You do not stop the old system to build the new one. You build the new system in pieces, around the old one, routing traffic to the new piece only once it works, while the rest of the app keeps running unchanged.

In practice, for a typical business application, that looks like:

  1. Put a routing layer in front of the legacy app. Often just a reverse proxy or API gateway that decides, per request, whether to serve from old or new.
  2. Pick the least risky, most isolated module first. Not the core order engine. Something like the reporting module or a self-contained admin screen, low blast radius if something goes wrong.
  3. Rebuild that module properly, with tests, then cut traffic over to it. The rest of the app does not notice.
  4. Repeat, module by module, moving toward the riskiest and most central pieces last, once the team has confidence and the routing pattern is proven.
  5. Retire the legacy codebase only when nothing routes to it anymore.

This is slower to show a fully new system, but it never freezes feature delivery, never doubles your running cost for a year, and every piece ships and gets tested with real traffic before the next piece starts. If the process itself is unclear before you touch code, map it first. Strangling an undocumented process just moves the confusion into new code.

When a Rewrite Is Actually the Right Call

I have recommended full rewrites, but only when most of these are true at once:

Condition Why it matters
The technology is genuinely dead No hires available, no security patches, vendor has stopped support entirely
The architecture actively blocks the business Cannot add a second warehouse, a second currency, a second country, without rebuilding the core
The system is small enough to rewrite in under three months Small blast radius, small sunk-cost risk if wrong
You have the original team or full documentation The edge-case knowledge problem does not apply if you actually know all the logic
The business can tolerate a hard cutover Rare for anything customer-facing at scale

Notice how narrow that list is. A multifinance company I worked with had a loan origination system on a genuinely unsupported stack, no available developers, no vendor patches, and a system small enough (a handful of screens, one core workflow) that a rewrite fit inside a single quarter. That is a legitimate rewrite. A retail chain in Tangerang wanting to "modernize" a working point-of-sale system because the code looked old was not: the system worked, the team could hire for the stack, and the actual complaints were three specific slow reports, which we fixed with targeted refactoring in three weeks instead of a rewrite that would have taken a year.

Questions to Ask Before You Decide

Before committing either direction, force honest answers to these:

  • What specifically is broken: performance, one workflow, hiring, security, or is it just aesthetic discomfort with old code?
  • Can we name the single most painful module, and could fixing just that module solve 80 percent of the complaint?
  • What is our tolerance for running two systems in parallel, in months and in budget?
  • Who currently understands the edge cases in this system, and are they still reachable?
  • If we are wrong about scope by 50 percent, which approach fails safer?

That last question usually settles it. A strangler approach that overruns delivers a working, improved system a bit later. A rewrite that overruns delivers nothing, for a long time, while the bills keep coming.

The Practical Takeaway

Refactor incrementally by default, and reserve a full rewrite for the narrow case where the technology is dead, the system is genuinely small, and the business can absorb a hard cutover. Before you approve either path, get a straight answer on what specifically is broken, because "it feels old" is not a scope, and it is not worth a year of frozen delivery. If you want a second opinion on a legacy system before a big rewrite decision gets made, that is the kind of call worth bringing in outside eyes for, and it is part of what I do through my partnership model.