Engineering

The real cost of a full rewrite

13 min readSiaSol

Nobody decides to rewrite a system because of a technical argument. They decide because shipping has become slow and painful, and a rewrite is the only option that feels like action. Here are the three questions that make the decision honestly, and the number that is never on the slide.

The case for a rewrite is almost always presented as a technical one and almost never is. The real driver is that every change has become slow, that estimates have stopped being believable, and that the team has lost confidence in its own ability to predict what a two-week task will cost. Those are real problems with real business consequences, and a rewrite is one of several possible responses to them — but it is the response that feels most like doing something, which is why it wins arguments it should lose.

We have been on both sides of this. We have recommended rewrites and delivered them, and we have talked clients out of rewrites that we would have been paid handsomely to perform. The difference between the two cases was never the quality of the existing code, which was poor in both. It was whether the thing making change expensive was something a rewrite actually removes.

What actually makes change expensive

Before costing a rewrite, it is worth being specific about where the time goes, because the answer is frequently not the code. In the systems we are asked to assess, the recurring causes are these.

  • No test suite, so every change requires manual verification of things unrelated to the change.
  • A deployment that takes an afternoon and a person, so changes are batched and each release carries more risk.
  • No staging environment that resembles production, so surprises are discovered by customers.

None of those three is a property of the code. They are properties of the engineering practice around it, they can be fixed without touching the architecture, and they are typically a quarter of work rather than a year. More importantly, they have to be fixed anyway: a rewrite delivered into the same practice produces a new system that is also slow to change, which is the single most common way a rewrite disappoints everyone involved.

So the first useful exercise is to take the last ten changes the team shipped and write down, for each, where the elapsed time actually went. Not the estimate — the elapsed time, including the waiting. In most cases a majority of it is in verification and deployment rather than in writing code, and that result reframes the whole conversation before any architecture has been discussed.

Question one: what cannot be fixed in place?

There is a short list of problems that genuinely resist incremental repair, and it is worth knowing them because they are the only honest technical case for a rewrite. A data model that cannot represent something the business now needs — a customer who can belong to two organisations, say, when the schema assumes one — is a real one, because every feature built on top of it has to encode the workaround and the workarounds compound.

A runtime that is out of support and cannot be upgraded because a dependency does not exist for the newer version is another, and it is the one that eventually forces the issue regardless of anybody’s preference, because at some point a security patch stops being available. The important detail is that this is a dated problem: it has a deadline, which makes it plannable, and it is usually smaller than it looks because the blocking dependency is often one library rather than the whole stack.

Four reasons are not on the list.

  • The language is unfashionable.
  • The framework is old but supported.
  • The code is ugly.
  • A new team dislikes the conventions of the old one.

Each of those is a real source of friction and none of them justifies the cost, because the friction they cause is roughly linear in the work you do and a rewrite is a large fixed cost paid before any of it is relieved.

Question two: who holds the knowledge?

A system that has been running for eight years encodes thousands of decisions, most of them undocumented and many of them responses to a real problem that nobody remembers. The strange conditional that excludes a specific customer type from a discount is there because of an incident in 2019. A rewrite either carries those decisions forward or rediscovers them one support ticket at a time, and which of those happens depends entirely on whether the people who made them are still available to ask.

This is the question that most often changes the answer, and it has a brutally simple form: if the two people who understand the current system left tomorrow, could the rewrite still be completed? If the honest answer is no, the rewrite is not a twelve-month project, it is a twelve-month project plus the time it takes to recover knowledge that currently exists only in conversation, and that second term is unestimable.

Write the rules down first

If you are going to rewrite, the first deliverable is not architecture, it is a document listing the business rules the current system enforces, written by reading the code and confirmed by the people who remember why. This is tedious, it takes a few weeks, and it is the single highest-value artefact of the entire project — including in the case where the rewrite is then cancelled, because the document outlives the decision and makes the existing system safe to change.

Question three: what stops while you build?

This is the cost that is never on the slide. For the duration of the rewrite, every new feature has to be built twice or postponed, and in practice it is postponed, which means the business stops receiving changes for as long as the project runs. A twelve-month rewrite is therefore not a twelve-month engineering cost; it is a twelve-month engineering cost plus twelve months of a competitor shipping things you cannot.

Teams handle this by promising to build features in both systems, and that promise does not survive month three. It cannot, because building twice halves the team’s output at exactly the moment the rewrite is behind schedule, and the rational local decision is always to pause the old system. That decision is made quietly, by nobody in particular, and the feature freeze that everybody said would not happen is in place by the summer.

So price it honestly at the start. Ask what the business expects to ship in the next four quarters, and ask which of those things it is prepared to not have. If the answer is that all of them are essential, then the rewrite as scoped is not affordable regardless of the engineering estimate, and the conversation needs to move to an approach that ships continuously.

What gets costedWhat usually does notRough size of the gap
Building the new systemFeatures not shipped meanwhileComparable to the build itself
Data migrationRunning both systems in parallelMonths of double infrastructure
The happy pathUndocumented business rulesWeeks of archaeology
Developer trainingRetraining everyone who uses itOften the largest single item
The cutover weekendThe quarter of elevated incidentsPlan for three months

Replace it in pieces instead

The approach that avoids most of the above is to put the new system in front of the old one and move functionality across one route at a time, leaving the old system serving everything that has not been moved yet. Each piece ships to production on its own, each is individually reversible, and the business keeps receiving changes throughout. It is slower in total engineering hours and dramatically cheaper in risk.

The honest drawback is that for a period you are running two systems and the seam between them is real work: shared sessions, a consistent view of the data, and a routing layer that somebody has to own. We have seen that seam be the hardest part of the project. It is still preferable, because the seam is a known engineering problem with a known shape, whereas a big-bang cutover concentrates all the unknowns into a single weekend.

It also produces a decision point every few weeks instead of one at the end. After three routes have moved, everybody involved knows what a route costs, and the remaining programme can be re-estimated against evidence rather than optimism. That is the property that most reliably prevents the twelve-month project that turns into thirty.

Two shapes for the same programme: one cutover carrying all the risk, against a sequence of individually reversible moves.

The new system will also get old

The second-system effect is well documented and still catches careful teams, because a rewrite is the one moment when every deferred idea can finally be included. The result is a system that is more general than the problem requires, and generality is the main thing that makes software hard to change. The old system is slow to change partly because it accumulated features; the new one can be slow to change on day one because it anticipated them.

The guard against it is a written scope that says the new system does exactly what the old one does, plus the one thing that was impossible, and nothing else. Every addition beyond that goes on a list to be built afterwards, as a normal change, measured against the normal bar. This is unpopular and it is the difference between a rewrite that finishes and one that is still three months away for two years.

Price it against the cost of staying

A rewrite is almost always compared against nothing, which is why it always looks worth doing. The honest comparison is against the cost of the next two years on the current system — the support hours, the manual workarounds, the features that get quoted at three times what they should because of where they have to go. That number exists and nobody has written it down, and until somebody does the rewrite is being judged against an imaginary alternative of zero.

Building that number takes about a day. Count the support tickets that exist only because of a known defect, the hours spent each month on a report somebody assembles by hand, and the two or three features that were quoted and then quietly dropped because the estimate was absurd. Multiply by twenty-four months. The result is frequently large enough to justify serious work and frequently not large enough to justify starting over, and both of those are useful answers.

The comparison also has to include the cost of running both systems at once, which is the line item that is always forgotten. For the whole of a phased replacement you pay for two sets of hosting, two sets of licences and the people who keep both alive, and that overlap routinely lasts twice as long as planned. It is not a reason to avoid the work; it is a reason to put it in the budget instead of discovering it in month seven.

The feature freeze nobody agreed to

Every full rewrite contains an unspoken clause: the business stops asking for things until it is finished. Nobody signs that clause and nobody believes it when it is pointed out, and then eighteen months in there are two backlogs — the features being rebuilt and the features requested since the rebuild started — and a team that has to deliver both in both codebases. That is the mechanism by which rewrites become permanent rather than late.

Which is an argument for short phases

The defence is to make each phase short enough that the freeze is survivable. A six-week slice that moves one capability can be protected; an eighteen-month programme cannot. It also means every phase ends with something in production, so the work has a floor — if the programme is cancelled in month four, the first three phases are still delivered rather than abandoned as a branch nobody merged.

There is a second-order effect worth naming. A team that ships every six weeks learns what the new system is actually like to operate, and feeds that back into the next slice. A team that ships once at the end learns all of it on the same day, which is also the day the old system is switched off. The phased version costs more in integration work and buys information at the only time information is still useful.

Two systems need one source of truth

The hardest part of a phased replacement is not the code, it is deciding which system owns each piece of data while both are running. If the answer is "both", you have built a synchronisation problem, and synchronisation between two schemas that disagree is where phased migrations actually fail. Every entity needs a single owner for the duration, with the other side reading rather than writing, even when that makes one screen temporarily awkward.

That ownership decision is best made per entity and written down, because it changes as phases land. Customers might move to the new system in phase one while orders stay in the old one until phase four, which means for three phases an order references a customer that lives somewhere else. That is workable with a documented ownership table and a read-only view, and it is chaos without one.

  • One named owner per entity, for the whole overlap.
  • The other side reads; it never writes the same field.
  • A written ownership table, updated as phases land.
  • A reconciliation report, read by somebody weekly.
  • A decided answer for which system wins a conflict.

And there should be a reconciliation report from the first week of the overlap — a scheduled comparison of the two systems on the entities they share, with the differences listed. It will not be empty, because no synchronisation is perfect, and the point is to know the size of the gap continuously rather than to hear about it from a customer. A drift of four records is a Tuesday task; the same drift discovered at cutover is a delayed cutover.

What we usually recommend

In the majority of assessments we have done, the recommendation has been a quarter of work on tests, deployment and observability, followed by a re-assessment. It is unglamorous and it has a useful property: if after that quarter changes are still slow, the rewrite case becomes much stronger and much better evidenced, and if they are not, you have saved a year. Either outcome is better informed than the decision available at the start.

When we have recommended a rewrite, it has been for the first two reasons on the list — a data model that cannot represent the business, or an unsupported runtime with a deadline — and it has been scoped as a sequence of route moves rather than a cutover. Twice the programme was stopped halfway because enough value had been recovered that the remainder did not justify itself, and both times stopping was the correct call rather than a failure.

  1. Audit where the last ten changes actually spent their time.
  2. Name the thing that cannot be fixed in place, or admit there is none.
  3. Write down the business rules before any architecture.
  4. Price the features that will not ship during the work.
  5. Move one route, then re-estimate against what it cost.

The decision deserves a week, not a quarter, and the week should be spent gathering the four numbers above rather than designing the replacement. Designing first is how a rewrite becomes inevitable before anybody has established that it is necessary, because a team that has spent a month on an architecture will defend it, and quite reasonably so.

Frequently asked questions

01Is bad code ever a sufficient reason on its own?

Almost never, because the friction it causes is roughly proportional to the work you do while a rewrite is a large cost paid up front. The exceptions are a data model that cannot represent the business and a runtime that can no longer be patched; those have deadlines, which makes them plannable rather than aesthetic.

02How long does a rewrite actually take?

Longer than the estimate, and the overrun is concentrated in two places nobody costed: recovering undocumented business rules, and the tail of incidents in the quarter after cutover. If you want a usable figure, move one route first and multiply by the number of routes.

03Can we keep shipping features during a rewrite?

Only if you move functionality across incrementally. The promise to build every feature twice does not survive month three, because doubling the work halves output exactly when the project is behind, and the freeze everybody said would not happen arrives quietly.

04What is the cheapest thing to try first?

A quarter on tests, deployment and observability, then re-assess. If change is still slow afterwards the rewrite case is stronger and far better evidenced; if it is not, you have saved a year. In most assessments we have done, this is where the time was actually going.