When a rewrite is the right call, and when it is not
Picture a Django monolith that a previous vendor built over three years. The codebase has 47 apps, no test suite, and a migration history that has been squashed twice without anyone recording what the squashes resolved. Deployments take 45 minutes because the build step runs a full data migration on every push. The previous vendor left eight months ago. No one on the client side can explain why the billing module calculates tax the way it does.
The right move is to spend two weeks reading the code before recommending anything. If a rewrite is the answer, it has to come with a specific reason: the cost of understanding the existing code exceeds the cost of writing a replacement. You can only show that if you track the hours. Say that in two weeks you have fully understood four of the 47 apps. At that rate, mapping the rest would take five months before you could write a single new feature. A rewrite, scoped to the six apps the business actually uses, might be a three-month job.
That calculation is the only honest basis for a rewrite. You do not rewrite because the code is ugly. You rewrite because the effort to safely modify the existing code is demonstrably greater than the effort to replace it.
The far more common case is the opposite. Take a manufacturer who asks to "modernize" a PHP application that runs their inventory and purchase orders. The code is procedural PHP 5.6, written years ago, with no framework. Every page is a single file that mixes SQL, HTML, and business logic. By any aesthetic measure, it is a mess.
But it runs the warehouse correctly. The purchase order workflow matches how the store manager actually thinks about stock. The reporting queries, buried inline in PHP, produce numbers the accountant has verified against Tally for years.
That is not a rewrite. The work is to upgrade PHP to 8.2, which takes about a week of fixing deprecated function calls and mysql\_ extensions. Extract the database layer into a single include file so that the connection logic lives in one place. Add a migration tool — Phinx — and create an initial migration that represents the current schema. Then start pulling the most-modified pages into a lightweight routing setup, one at a time, as the client requests changes.
That path is on the order of 40 hours. A rewrite would be closer to 400. The system keeps working through the transition. The store manager does not have to learn a new interface. The accountant's verified numbers do not change.
The temptation to rewrite usually comes from a specific feeling: the code is hard to read, therefore it must be hard to maintain, therefore it should be replaced. That syllogism breaks at the second step. Code can be hard to read and still be cheap to maintain if the changes you need are small and infrequent. A ten-year-old PHP file that the client touches twice a year does not need to become a React frontend with a Laravel API. It needs to keep running, and it needs to accept the occasional small change without falling over.
There is a middle case we see often enough to name. A client has a working application but a broken deployment. The code is fine; the pipeline is not. They have no CI, no staging environment, deploys happen over FTP, and the last person who knew the server password left. This is not a rewrite problem. It is an infrastructure problem. We containerize the application, set up a CI pipeline, create staging and production environments, and document the deploy process. The code stays. The way it ships changes.
We treat rewrites the way we treat surgery: avoidable unless the alternatives are worse, and the alternatives are usually worse only when you can measure it. The measurement does not need to be precise. But it needs to exist. "The code feels old" is not a measurement. "We spent 80 hours last quarter fixing bugs in the authentication module, and the module has 600 lines" is a measurement. The first does not justify a rewrite. The second might.
When a client asks us whether they should rewrite, we ask them to let us spend a week in the code first. Sometimes we come back with a rewrite recommendation, as in the Django monolith case above. More often we come back with a list of specific fixes that cost a fraction of a rewrite and address the actual pain. The pain is rarely "the codebase is old." The pain is usually a specific bug, a slow page, a feature that takes too long to ship, or a deploy that breaks at 2am. Those have targeted solutions. A rewrite is a targeted solution for exactly one problem: the code is more expensive to understand than to replace.