Spectre
// PUBLISHED05.09.26
// TIME10 MINS
// TAGS
#LEGACY MODERNISATION#REFACTORING STRATEGY#TECH DEBT#FOUNDERS
// AUTHOR
Spectre Command

T

he calendar invite said "Architecture discussion" and somebody had appended "(3)" to the title.

Same room, same six people, same two columns on the same whiteboard. The two engineers who want the rewrite have better arguments than last time. The CTO, who has lived through one, is against it and cannot say precisely why in a way that survives the meeting. Nobody is wrong. Nobody decides. The invite for next month is already provisional.

The rewrite vs refactor argument about legacy code stays unresolved because it gets run as a debate between two positions rather than as a decision with inputs. Score the system on six axes instead, weight them, and the number tells you which of three paths you are on. Most systems land in the middle band, which is neither of the two things people are arguing about, and the unit of the decision turns out to be a component rather than the application. Framing it as "should we rewrite the app" is usually the first mistake, because almost no whole application scores high enough to justify one.

How do I decide between a rewrite and a refactor?

Score the specific thing you want to replace, not the codebase in general. Zero to three on each axis, multiplied by the weight, summed.

AxisScore 0Score 3Weight
IsolationThreaded through everything, no seamA clean interface already exists×3
Domain clarityBehaviour discoverable only by reading codeDocumented, understood, agreed×3
Characterization coverageNo tests describing current behaviourImportant paths pinned by tests×3
Team knowledgeAuthor left, nobody understands itTwo or more people know it well×2
Appetite for slower deliveryBoard expects the roadmapNamed sponsor has agreed explicitly×2
Blast radiusPayments, auth, customer dataInternal tooling×2

Forty-five is the maximum. Below 20, refactor, because a rewrite will fail and the failure will be expensive. Between 20 and 32, replace incrementally behind an interface, which is where most real systems land. Above 32, a bounded rewrite of that component with a date on it is defensible.

The fourth row is where teams reliably reverse the sign, and it is worth being blunt about. If nobody left in the building understands the current implementation, that is an argument against rewriting it, not for it. A rewrite has to reproduce behaviour, and behaviour nobody understands cannot be reproduced deliberately, only rediscovered by shipping something different and waiting for complaints. The instinct runs the other way because incomprehensible code is unpleasant, and unpleasant reads as disposable.

Run the rubric twice, once on the module everyone complains about and once on the whole application. The gap between those two numbers is usually the real finding, and it is nearly always large.

Why do most rewrite plans die?

Because they require you to stop shipping features for nine months and nobody will say that sentence out loud in the meeting where the plan is approved.

The plan does not say it either. It says the team will "run both workstreams", which survives contact with reality for about six weeks. Then a customer escalation lands, or a competitor ships something, or a renewal needs a feature, and the rewrite becomes the thing that gets paused. Paused rewrites do not resume. They sit at sixty percent, and now you maintain two systems, fix every bug twice, and hold the knowledge of which behaviours have been ported in one person's head.

Any plan whose success depends on a feature freeze is already dead, whatever the scoresheet says, and the honest version of that plan is one that runs alongside production the whole way. That means the strangler fig pattern, named by Martin Fowler in 2004: put the new implementation behind the same interface, route a slice of traffic to it, compare, expand. It is slower on paper and it is the only shape that survives a quarter with a real business attached to it. The mechanics of doing that without stopping the company are their own subject, covered in rewriting a system without downtime.

There is a related trap in the shape people choose. A rewrite is frequently packaged as a move from a monolith to services, which doubles the risk by changing the architecture and the implementation at the same time, and makes it impossible to tell which of the two caused any given regression. Those are separate decisions and the monolith versus microservices question deserves its own answer rather than riding along.

What do rewrite estimates always leave out?

The behaviour customers depend on that nobody ever wrote down as a requirement.

Joel Spolsky made this argument in April 2000, in an essay about Netscape throwing away its codebase, and the mechanism he described has not changed in twenty-five years. Every one of those strange conditionals in the old code is a bug fix. Each represents a real problem, found in production, patched under pressure, and never written into a specification because the specification was finished years earlier. Discard the code and you discard the accumulated fixes. You rediscover them one support ticket at a time, in the fortnight after cutover, which is precisely when your confidence and your credibility are lowest.

He is right about the mechanism and wrong as an absolute rule, which is why the essay gets cited by people who have already decided. It argues against rewriting, and what it actually argues against is rewriting without first capturing behaviour. Characterization tests, in the sense Michael Feathers described in 2004, are the tool for that: tests that assert what the system currently does rather than what it should do, including the parts that look like bugs. Writing them is unglamorous and it converts undocumented behaviour into a specification you can build against.

Four categories go missing from nearly every estimate I have reviewed. Data migration and backfill, which is often larger than the application work and always discovered late. The long tail of integrations, where the third and fourth ones each cost as much as the first. Reporting and exports, because customers have built their own workflows on the exact shape of a CSV you consider an implementation detail. And internal admin tooling, which never appears in a spec and which your support team cannot do their job without.

A useful correction factor is two to three times the initial estimate. That is not pessimism. It is the observation that the estimate covers the part of the system the estimator can see.

When is a rewrite genuinely the right call?

Four situations, and they are real enough that dismissing them would make everything above less useful.

The runtime or framework is end of life with no security patches, and the upgrade path was abandoned upstream. That is a deadline set by someone else and it converts the decision into a scheduling problem.

The data model encodes an assumption that is now false. This is the strongest genuine case and the most under-rated, because it is the one where refactoring costs more than rewriting rather than less.

Take a schema built on one user per account, in a company that has started selling to teams. The refactor path means introducing an organisation concept while every existing query, permission check, billing calculation and export continues to assume the old shape. You write a compatibility layer. Then every new feature has to work correctly under both models, so the layer grows rather than shrinks, and it cannot be removed until the last caller migrates, which requires a data backfill nobody wants to run on production. Two years later the compatibility layer is the most complicated code in the system and both models are still live. The incremental path has a genuine end state here, and the cost of reaching it exceeds the cost of building the right model once and migrating to it deliberately.

The component is small enough that understanding it costs more than replacing it. Genuinely true below a few thousand lines, and this is why the component framing matters so much.

Licensing or vendor cost forces a move, which is rare and unambiguous when it happens.

What none of those four say is that the code is bad. Unpleasantness is not on the list, and it is the reason given in most of the meetings where a rewrite gets approved.

Why do teams choose rewrite for reasons they do not state?

The stated reasons are usually not the operative one. Working in the codebase is miserable, and misery is not an argument anyone can put in a planning document.

The tell is consistent. The conclusion stays fixed while the supporting arguments rotate between meetings. Performance in the first meeting, maintainability in the second, hiring difficulty in the third, and the recommendation is identical every time. That is a decision that has already been made, looking for a justification that clears the finance conversation.

None of that is dishonest. Working in a codebase you dislike, every day, is a genuine cost, and it shows up as attrition and slow delivery long before it shows up in an architecture review. The problem is that a rewrite is an extremely expensive retention tactic. The new system acquires the same properties within eighteen months under the same pressures, deadlines and staffing that produced the first one, unless something about those pressures changed. If two senior engineers are threatening to leave, that is worth solving directly, and there are interventions that cost a fortnight rather than a year.

The cost of doing nothing is real too, and it is worth putting a number against it rather than leaving it as a feeling, which is what the cost of technical debt is for. A decision between two priced options is a different conversation from a decision between a priced option and a vibe.

I have led one rewrite that should not have happened. Fourteen months, and it shipped, which is the part that makes it hard to learn from. The honest retrospective is that extracting the single worst module behind an interface, plus three months of characterization tests over the rest, would have delivered most of the benefit inside a quarter. I argued for the large version and I was persuasive about it. Worth knowing when you read anyone's opinion on this, including mine, that the long staffed rewrite is also the most profitable engagement shape a firm like ours can sell. Discount our enthusiasm accordingly, and ask any partner proposing one what the bounded version looks like.

If the answer to the whole question turns out to be that the architecture cannot support what sales keeps promising, that is a different problem with its own shape, and it sits inside the wider set of architecture blockers attached to revenue.

Questions engineering leads ask before committing

How do I know if we should rewrite or refactor? Score the specific component on isolation, domain clarity, test coverage, team knowledge, delivery appetite and blast radius. Under 20 out of 45, refactor. Over 32, a bounded rewrite of that component is defensible. Most things land between, which means incremental replacement behind an interface.

How long does a rewrite actually take compared to the estimate? Two to three times, in most projects I have seen or reviewed. The overrun concentrates in data migration, integrations, exports and internal admin tooling, none of which are usually in the original scope.

Is it safe to rewrite if nobody understands the old code? No, and this is the assumption most often held backwards. Reproducing behaviour requires knowing what it is, so an unreadable system is a reason to write characterization tests first rather than a reason to discard it.

Can we rewrite while still shipping features? Only incrementally, routing traffic between old and new behind a shared interface. Any plan that needs a feature freeze will be paused at sixty percent by the first customer escalation, and paused rewrites do not resume.

When is rewriting genuinely cheaper than refactoring? When the data model encodes an assumption that is no longer true, when the runtime is end of life, or when the component is small enough that understanding it costs more than replacing it. An unpleasant codebase on its own is not one of these.

Score the module, then score the whole application, and put both numbers on the whiteboard at the start of the fourth meeting. The argument tends to end quickly once everyone is looking at the same two figures, and what usually follows is a much smaller piece of work than either side was proposing.

// END_OF_LOGSPECTRE_SYSTEMS_V1

Is your current architecture slowing you down?

Stop guessing where the bottlenecks are. We partner with founders and CTOs to audit technical debt and execute zero-downtime system rewrites.

Book an Architecture Audit