mbvlabs.

Should I Rewrite My Legacy Codebase? A Practical Decision Framework

A legacy rewrite should begin with a measurable target and a reliable way to compare old and new behaviour. This framework helps teams choose between retaining, refactoring, replacing, and rewriting before committing to a migration.

01 / Published

July 17, 2026

02 / Updated

July 17, 2026

03 / Topics

Legacy Code, Software Modernisation, Refactoring, AI, Technical Leadership

Should I Rewrite My Legacy Codebase? A Practical Decision Framework

I have worked on a rewrite where the decision made sense. At ChatSheet, I ported a Rails API to a Go API with a Svelte frontend. The work took a couple of months, was driven by performance and stability concerns, and delivered better performance after the migration.

We protected the migration with golden files containing representative JSON responses from the Rails API. A golden file is a stored output that a test can compare against a new result, which meant the Go implementation could be checked without depending on Rails internals. When someone asks, “should I rewrite my legacy codebase?”, my answer starts with three conditions: the rewrite needs a measurable benefit, the existing system needs to be economically incapable of reaching that target, and the team needs a reliable way to verify behaviour during the move.

What problem would a rewrite actually solve?

Teams usually begin considering a rewrite after delivery has slowed, reliability has deteriorated, dependencies have become unsupported, or infrastructure costs have become difficult to ignore. A different language may promise better performance, stronger tooling, safer memory management, or access to a wider hiring market. Those benefits only support a business case once the team defines how it will recognise success.

Performance could mean reducing the response time experienced by 95 per cent of requests, often called p95 latency, from 800 milliseconds to 200. Reliability could mean cutting a recurring class of incidents or reducing failed background jobs. Hiring could mean shortening onboarding time because more engineers understand the target stack, while operating cost could be measured directly against the monthly infrastructure bill.

Pressure Useful baseline Possible rewrite target
Slow delivery Lead time for a normal feature Reduce lead time by an agreed amount
Reliability Incidents or failed jobs per month Remove a documented failure class
Performance Response time, throughput, or memory use Reach a tested threshold
Operating cost Hosting, licensing, and support cost Recover migration cost within an agreed period
Hiring Time to hire and onboard Use a sustainable stack with a wider candidate pool
Unsupported technology Security and dependency limitations Move to a supported runtime by a fixed date

A team without a baseline cannot know whether the rewrite worked. It can only say that the code is newer, which is a poor return on months of engineering. The first useful modernisation task is therefore measurement, even when everybody already agrees that the existing code is frustrating.

MBV Labs starts technical discovery and modernisation work from the business pressure because architecture preferences alone cannot establish the value of a rewrite.

Rewrite, refactor, replace, or retain: the short version

A rewrite is one of four practical choices. The same codebase may use several of them across different components, especially when some parts remain stable while one boundary causes most of the cost. Classifying each problem separately prevents a local issue from turning into a programme to replace everything.

Path Choose it when First reversible step
Retain The system meets current needs and changes infrequently Document ownership, dependencies, and operating requirements
Refactor The stack remains viable and the pain is localised Improve one costly path while preserving external behaviour
Replace The capability is not product differentiation and an existing service fits Trial the replacement behind one integration boundary
Rewrite The foundation blocks a valuable, measurable outcome Port one representative slice and compare it with production behaviour

Retaining a system can be a deliberate engineering decision. Stable software that performs an unchanging job does not become a liability merely because its framework is unfashionable. The relevant questions concern support, ownership, reliability, cost, and the frequency with which the business needs it to change.

Replacement deserves more attention than it usually receives. Authentication, invoicing, search, email delivery, and other capabilities may be cheaper to buy than to recreate, depending on their importance to the product. A team should reserve custom software for areas where its requirements or operating constraints genuinely differ.

When a rewrite may be justified

The expected benefit is large and measurable

A rewrite needs enough potential value to cover implementation, migration, parallel operation, and the defects that appear after release. Better performance or tooling can qualify, provided the improvement affects customers, operating cost, or delivery capacity. A wider hiring market can also matter when the current stack makes recruitment or onboarding consistently difficult.

ChatSheet had a concrete performance and stability motivation for moving from Rails to Go, and the replacement delivered better performance. I do not have a public benchmark that turns that outcome into a percentage, so it should remain a qualitative example rather than evidence that Go will improve every API. The useful lesson is the sequence: establish the practical pressure, choose a target stack suited to it, and verify the result against the old behaviour.

The existing foundation cannot reach the target economically

A slow endpoint rarely justifies replacing an application. Measurement may reveal one database query, an overloaded integration, or an inefficient data model that can be corrected inside the existing system. A short investigation is cheaper than discovering the same bottleneck after rebuilding it in another language.

The rewrite case becomes credible when the limitation is structural or repeated attempts at incremental improvement have failed. An unsupported runtime, an architecture that cannot satisfy a required reliability boundary, or pervasive operating costs may qualify. The team should still prove that conclusion through profiling, dependency analysis, and a representative implementation slice.

The team can control scope and verify behaviour

Stable requirements make a rewrite easier to define. A clear migration owner and cutover plan reduce the chance that two systems remain active indefinitely. Language-independent tests create a shared definition of behaviour that both implementations must satisfy.

The ChatSheet golden files worked because they captured JSON responses at the API boundary. The Go service could receive the same inputs and be checked against the responses produced by Rails. This approach preserved the useful contract while allowing the internal implementation to change completely.

Golden files need deliberate review because an old response may contain behaviour that should be removed. Their job is to reveal a difference, leaving the team to decide whether that difference is intentional. They provide evidence during a migration without turning every historical quirk into a permanent requirement.

When a rewrite is more dangerous than the legacy code

The existing system has years of accumulated behaviour, including rules nobody remembers adding. Some rules are obsolete, while others quietly support billing, permissions, integrations, or unusual customer workflows. Rebuilding from visible requirements alone can remove both categories without showing the team which one it removed.

Warning signs include:

  • There is no agreed migration sequence or cutover owner.
  • Critical workflows have little automated coverage.
  • Product requirements are moving every week.
  • The existing team must continue building features that could attract new customers.
  • Few people understand the target language or framework.
  • Data migration and rollback have not been designed.
  • The estimate covers implementation while excluding parallel operation and post-release defects.

Unfamiliarity with the target stack deserves particular attention. Developers can produce a clean first version while missing the failure modes, conventions, and operational practices that experienced users of the stack take for granted. A rewrite motivated by maintainability can therefore leave the organisation with a system that fewer current engineers know how to debug.

Even unusually well-controlled rewrites introduce regressions. Bun’s official Zig-to-Rust rewrite report records 19 known regressions after an AI-assisted port backed by a large test suite and extensive review. That result demonstrates the need to plan for defects as normal migration work.

The smallest useful assessment before deciding

Measure the current pain

Begin with the metric the rewrite is expected to improve. Record its current value, the target value, and the period over which the benefit must repay the migration cost. Include the cost of leaving the system alone so the comparison reflects both options.

The metric needs to match the stated pressure. A reliability rewrite should be evaluated through incidents, failed operations, or a specific class of defects. Counting lines of new code, completed modules, or translated files measures activity without showing whether the business problem is getting smaller.

Capture behaviour independently of the implementation

Choose the workflows whose failure would affect customers, revenue, security, or operations. Exercise them through an external boundary such as an API, command, queue, or browser flow. Store representative inputs and outputs so the same checks can run against the replacement.

Golden files are useful when responses are structured and reasonably stable. They can capture nested JSON, generated documents, database exports, or command output with very little test code. Dynamic fields such as timestamps and identifiers should be normalised so the test focuses on meaningful behaviour.

Test coverage percentages provide limited guidance here. A system can report high line coverage while missing its most important integration, and a smaller set of boundary tests can protect the workflows that matter during a rewrite. The assessment should identify behavioural gaps instead of pursuing an arbitrary percentage.

Port one representative slice

Select a slice that includes real domain logic, data access, and at least one external boundary. A trivial health endpoint proves that the new framework can start a server, which the framework documentation already established. The trial should expose enough of the actual system to test the migration assumptions.

Compare the slice on correctness, implementation time, performance, observability, and how easily the current team understands it. Record any compatibility layer, duplicated data, or operational work the slice requires. This produces a local result that can inform an estimate without pretending one experiment has revealed every edge case.

The trial also gives the team permission to stop. If the target stack fails to produce the expected advantage, keeping the experiment small makes that discovery inexpensive. A rewrite proposal becomes stronger when it has survived an honest attempt to disprove it.

Price the cutover

Implementation is only one part of a rewrite. Data migration, integrations, deployment, monitoring, rollback, user communication, staff training, and parallel operation all consume time. The existing system may also need bug fixes and product changes while the replacement is being built.

A clean switch reduces the period of parallel operation, provided the team can complete and verify the replacement quickly enough. A phased migration lowers the blast radius while adding temporary routing and integration work. The estimate should reflect the chosen cutover strategy instead of assuming deployment will be a final, simple task.

The assessment should finish with a range and explicit assumptions rather than a single confident date. Requirements will change, hidden behaviour will surface, and the target technology will introduce its own learning curve. Naming those uncertainties gives decision-makers a more useful business case.

How to modernise a legacy application without a full rewrite

Many systems can reach their target through a sequence of smaller changes. This path keeps production feedback available and lets the team stop once the measured problem has been solved. It also preserves feature delivery when a long replacement programme would consume the whole roadmap.

  1. Add behavioural tests around the area that will change. Capture current outputs and review which behaviours should survive.
  2. Update the immediate dependency or runtime constraint. An incremental upgrade may remove the support or security problem without changing the application.
  3. Isolate the costly boundary. Put a stable interface around the database, external service, or module causing most of the friction.
  4. Move one production path. Route a small part of real traffic to the replacement and compare errors, latency, and outputs.
  5. Delete the old path after cutover. Temporary adapters need an owner and removal condition.
  6. Stop when the target is reached. Modernisation does not need to end with every old component replaced.

Martin Fowler’s Strangler Fig Application pattern describes routing functionality gradually from an old system to a new one. The approach makes each cutover observable and limits the amount of behaviour changing at once. It works best when the team can identify clear seams instead of maintaining two complete applications for years.

Incremental work still has a cost. Temporary interfaces, duplicated data flows, and mixed deployment models can become permanent architecture when nobody owns their removal. Each phase should therefore include a metric, a cutover condition, and something the team expects to delete.

Can AI rewrite legacy code?

Where AI reduces the work

AI can help engineers read unfamiliar code, trace dependencies, draft characterisation tests, translate repetitive structures, and review differences between implementations. Thoughtworks describes these uses in its field report on legacy modernisation with generative AI, including requirement extraction and system mapping. These activities address expensive discovery work that previously depended heavily on a small number of subject matter experts.

Mechanical translation is particularly suitable when the architecture and behaviour should remain stable. An agent can process many similar files, use compiler errors as a work queue, and apply the same correction repeatedly. Human engineers still need to define the translation rules, review exceptions, and decide when generated code is safe to merge.

AI also makes time-boxed migration experiments cheaper. A team can port one representative slice, generate a first set of tests, and compare target technologies before committing to a programme. This is where faster code generation improves the decision rather than merely increasing output.

What the Bun rewrite actually proves

Bun provides the strongest current example of AI changing rewrite feasibility. According to its official report, one engineer monitored workflows running up to 64 Claude instances and moved a codebase containing 535,496 lines of Zig to Rust in 11 days. The process used a language-independent TypeScript test suite, adversarial review agents, compiler feedback, CI across six platforms, and an estimated $165,000 of API usage at published prices.

The project also had a precise reason for changing languages. Bun wanted Rust’s compiler-enforced memory safety to address recurring use-after-free, double-free, and memory leak problems. The team preserved the existing architecture and behaviour through a mechanical port, which constrained what the agents needed to invent.

Those conditions make Bun evidence that a clean, AI-assisted rewrite can now be feasible in cases that previously could not justify a year of engineering. Teams should derive their own estimate from a representative slice because the eleven-day timeline depended on exceptional tests, infrastructure, domain knowledge, and review capacity. The report’s false starts and regressions are part of the evidence, since they show how much process sat around the generated code.

What AI still cannot decide

Legacy code contains only part of the system’s requirements. Customer expectations, manual operational work, old contract terms, and undocumented integrations may live outside the repository. An agent can explain the code it can see without proving that the visible code represents everything the business needs.

AI also cannot choose which old behaviours deserve preservation. That decision needs product knowledge and a person accountable for the outcome. Subject matter experts remain necessary to validate extracted requirements, especially where code behaviour and current business policy have diverged.

My broader position on using agents without losing engineering ownership applies directly to rewrites. Agents can accelerate comprehension, experiments, translation, and review when an engineer owns the system model and the cutover. The team still needs enough understanding to operate the replacement if its preferred AI tool disappears.

Frequently asked questions

Is it cheaper to rewrite or refactor legacy code?

Refactoring is usually cheaper for a localised problem because it preserves working behaviour and allows incremental delivery. A rewrite can become cheaper when the existing foundation blocks a valuable target and repeated incremental work would require more time or operating cost. The comparison needs to include migration, parallel operation, data movement, training, and post-release defects alongside implementation.

How much test coverage do you need before a rewrite?

There is no coverage percentage that makes a rewrite safe. Prioritise language-independent checks around critical workflows, integrations, permissions, billing rules, and data transformations. Add tests until the team can recognise unintended behavioural differences in the parts being migrated.

Can you rewrite a codebase without freezing feature development?

A short clean switch can limit the freeze when scope is stable and the replacement can be verified quickly. Phased replacement allows feature work to continue by moving one boundary at a time, with additional coordination and temporary integration costs. Teams with many customer-driving features in progress should include the opportunity cost of delayed work in the rewrite decision.

What I would do before approving the rewrite

I would begin by asking what the rewrite is for and which concrete metrics should improve. Then I would establish a behavioural baseline, test the target stack on one representative slice, and price the entire cutover. The resulting evidence should decide among retaining, refactoring, replacing, and rewriting.

My practical sequence is:

  1. Name the business or product pressure.
  2. Record the current metric and target.
  3. Capture critical behaviour independently of the source language.
  4. Port one representative slice.
  5. Compare total cost, risk, and expected benefit.
  6. Choose the least disruptive option that reaches the target.
  7. Assign a cutover owner and rollback plan before implementation expands.

A rewrite can be the right decision when the benefit is large, the scope is controlled, and the team can prove behavioural equivalence. AI has widened the set of rewrites that are technically feasible, while the business case and migration responsibility remain human decisions. If your team is facing this choice, review selected client work or contact MBV Labs for a focused technical assessment.