Skip to content
Aug 14, 2026ยท8 min read

Four numbers decide when to rewrite legacy software

Use four measurable signals to decide when to rewrite legacy software, when to keep patching, and how to defend the choice with evidence.

Four numbers decide when to rewrite legacy software

Rewriting a legacy system is justified when the cost and risk of changing it exceed the cost and risk of replacing it. Age, language, and architectural taste do not answer that question. Four measurements do: change failure rate, the share of the team that can safely change the code, the unsupported share of the runtime, and the cost of one serious incident.

I have seen teams authorize rewrites because a framework looked old, then discover that the old application changed twice a year and caused no trouble. I have also seen teams defend "incremental improvement" while one remaining maintainer carried a production system on an unsupported runtime. Both decisions came from instinct dressed up as engineering. Put the four numbers on one page and the argument gets much harder to fake.

Rewrite decisions need measurements, not an age limit

There is no useful age at which software automatically becomes a rewrite candidate. A fifteen-year-old service with stable interfaces, supported dependencies, good tests, and several confident maintainers may be cheaper to keep than a three-year-old service that fails on every release. Calendar age is a clue about where to inspect, not a decision criterion.

The same applies to language choice. COBOL, RPG, VB6, Delphi, or an old PHP version can create hiring and support problems, but a language label does not quantify either one. A well-understood COBOL batch job that processes a fixed file every night may deserve another decade. A small JavaScript service that only one contractor understands may already be an operational liability.

Start with a measurement window that includes ordinary delivery and at least one demanding business period. A quarter often works for teams that deploy regularly. A slower system may need a longer window, but do not wait forever for perfect data. Record uncertainty next to each number and improve the measurement as you go.

Use the same unit of analysis for all four measures. Decide whether you are judging one application, one deployable service, or a tightly coupled group that must move together. Measuring failures for a whole portfolio and maintainers for one repository produces a polished but meaningless scorecard.

The four numbers answer different questions:

  • Can the team change the system without breaking production?
  • How many people can make that change without supervision?
  • How much executing software no longer receives fixes from its maintainers?
  • What does the business lose when the system causes a serious incident?

None is a universal threshold. Establish a boundary that fits your risk tolerance, then write down what action follows when the system crosses it. The value lies in using the same rule before an outage, when nobody can bend it to support the conclusion they already want.

Change failure rate exposes the tax on every release

Change failure rate tells you what fraction of production changes require remediation because they caused degraded service, an outage, a rollback, or an urgent fix. DORA uses this idea as a software delivery performance measure. I would keep that definition narrow: count failures caused by a change, not every production incident that happened near a deployment.

Choose what counts as a production change before calculating the rate. Include application deployments, database migrations, configuration releases, scheduler changes, and infrastructure changes if the team controls them as one delivery process. Do not count a routine retry as a separate failed change, and do not let ten emergency commits after one bad release turn into ten independent failures.

The basic calculation is simple:

change_failure_rate = failed_production_changes / total_production_changes

The difficult part is classification. For every failed change, record the triggering change, the customer effect, the recovery action, and whether the defect came from code, data, configuration, or an unknown interaction. A ticket label such as change-failure is enough if people apply it consistently. Review disputed cases monthly with engineering and operations in the same room.

Do not compare raw rates across systems with radically different release patterns. One application may ship a hundred small changes while another ships one bundled release. Pair the rate with change volume and median recovery time. A 10 percent rate on ten reversible feature flags is not the same business risk as a 10 percent rate on quarterly database releases that take six hours to restore.

Trend matters more than a single reading. If the rate falls after tests, observability, and smaller releases improve, patching is buying back control. If it remains high after two or three focused attempts, the architecture may be defeating the delivery method. Look at recurring causes: shared mutable state, undocumented batch ordering, hidden database contracts, and deployment steps that cannot be rehearsed. Those patterns make every future feature more expensive.

Do not reward a team for avoiding changes. A system with no deployments has an undefined or deceptively perfect rate. Track demand that the team postponed because release risk was too high. When business changes queue up behind fear of touching production, the absence of failures is evidence of paralysis, not health.

Safe ownership is smaller than repository access

The safe-touch share is the proportion of the relevant engineering team that can independently make, review, deploy, and recover a meaningful change in the system. Repository access does not count. Neither does the ability to edit a file while the one expert dictates every step.

Define a meaningful change using work the system actually receives. It might be adding a field through the database, service, and user interface; changing a calculation with financial consequences; or modifying a nightly job without breaking restart behavior. The task should cross the boundaries where knowledge usually disappears.

Assess each engineer against four capabilities:

  • Explain the affected execution path and its dependencies.
  • Implement and test the change without copying an unexplained precedent.
  • Release it through the real production process.
  • Diagnose a failure and restore service without waiting for the system's oracle.

Count a person only when recent evidence supports all four. A paired change is useful for teaching, but it does not prove independent ownership. Reviews, incident participation, and a successful production change give better evidence than a self-rating survey.

Then calculate:

safe_touch_share = independent_safe_maintainers / relevant_engineers

Also record the absolute number. A 50 percent share sounds healthy until it means one person out of two. Conversely, four capable maintainers inside a team of twenty may cover a stable internal application adequately, provided on-call and leave do not remove all four at once.

This measure exposes a distinction teams routinely blur: documentation availability is not operating knowledge. A thousand-page runbook can explain screens and tables yet omit why settlement must precede reconciliation, which malformed records operators repair by hand, or how to resume a job without double-posting. Test the document by asking a new maintainer to use it under supervision. The questions they ask are part of the missing system specification.

A falling safe-touch share is often the earliest rewrite signal because it has a deadline hidden inside it. Retirement, resignation, or vendor departure can turn a difficult system into an unchangeable one overnight. If knowledge transfer raises the share and the newly trained people keep making changes six months later, patching still has a case. If every transfer attempt routes work back through the same expert, stop calling the situation training.

Unsupported runtime share measures exposure, not embarrassment

Unsupported runtime share is the proportion of the production execution stack that no longer receives security or correctness fixes from the responsible vendor or project. It should include more than the application language. Count operating systems, databases, application servers, language runtimes, major frameworks, drivers, and essential middleware.

Inventory what actually runs, not what the architecture diagram claims runs. Pull versions from hosts, container images, package locks, job definitions, and database queries. For a mainframe or midrange system, include compilers, transaction monitors, schedulers, and vendor components that sit in the execution path. Record the source of each support date so procurement folklore does not become policy.

A useful weighted calculation is:

unsupported_runtime_share = sum(weight_of_unsupported_components) / sum(weight_of_all_components)

Set weights by exposure and business role. An unsupported public application server deserves more weight than an isolated report converter used once a month. Keep the weighting simple enough that another engineer can reproduce it. If a committee needs an hour to explain why one component has a weight of 7.3, the precision is fictional.

Unsupported does not mean compromised, and supported does not mean safe. Unsupported means the maintainer has stopped supplying a normal path for fixes. Your team may compensate with isolation, virtual patching, strict input controls, or a paid extended-support contract. Write each compensating control beside the component and test whether it really applies to every entry point.

Watch for the unsupported core hidden under supported edges. A current browser, reverse proxy, and database do not rescue business logic that requires an abandoned runtime. Teams sometimes upgrade everything around the old core and report a low obsolete-component count. Weighting by execution importance prevents that bit of accounting.

Patching remains reasonable when unsupported components are isolated, stable, replaceable in place, and covered by controls whose operating cost stays low. It becomes harder to defend when an unsupported component accepts untrusted input, blocks operating-system upgrades, or forces the team to retain an entire obsolete deployment chain. At that point the exposure compounds: every surrounding upgrade must accommodate the oldest piece.

Incident cost turns technical risk into a business limit

Read the whole estate
The platform analyzes every language in the source tree together, including systems over a million lines.

The cost of one serious incident is the full business loss from detection through recovery and correction, not the cloud bill or the engineers' overtime alone. Use an actual incident when possible. If the system has not had one, build a scenario with finance, operations, security, and the business owner, and label every assumption.

Calculate cost in categories that someone can verify:

incident_cost = lost_margin
              + staff_hours * loaded_hourly_cost
              + customer_remediation
              + contractual_or_regulatory_cost
              + data_reconciliation
              + delayed_business_events

Use lost margin rather than gross transaction value unless transactions disappear permanently. Separate delayed revenue from lost revenue. Count manual work performed by operations, finance, support, engineering, and management. Include the days after service restoration when people reconcile records, correct duplicate actions, answer customers, and prepare required notices.

Avoid multiplying a frightening hourly number by the longest imaginable outage. Define the serious incident precisely: for example, four hours of order capture failure followed by one day of reconciliation, or an incorrect pricing run that reaches customers before detection. Then document volume, margin, labor rates, contractual terms, and recovery assumptions. Finance should be able to challenge the model line by line.

Use two costs if the system has distinct failure modes. An availability incident and a silent data-integrity incident rarely have the same shape. The latter can look cheap during the outage because nobody detects it, then become far more expensive during reconstruction. Averages conceal that difference.

Incident cost changes the rewrite argument because it sets the amount worth spending to reduce exposure. A fragile system that supports a minor internal workflow may rationally stay patched. A similarly fragile system that posts ledger entries or controls factory movement deserves a lower tolerance for the other three measures.

Do not turn incident cost into an expected-loss magic trick unless you have credible frequency data. Multiplying a guessed annual probability by a modeled consequence produces a neat currency figure with two weak inputs. Keep frequency evidence, consequence evidence, and uncertainty visible. Executives can make a decision without pretending the guess is actuarial science.

Put the four numbers on one decision sheet

A decision sheet should show current values, trend, confidence, an agreed boundary, and the action attached to crossing that boundary. One page is enough. Its job is to force explicit tradeoffs, not calculate an answer that management must obey.

Use a table like this and replace the example boundaries with your own:

MeasureCurrentTrendConfidenceDecision boundaryAction if crossed
Change failure rate18% of 50 changesRisingHigh15% for two reviewsFund replacement design
Safe-touch share2 of 14 engineersFallingMediumFewer than 3 peopleFreeze discretionary features
Unsupported runtime share35% weightedFlatMedium25% with external inputStart containment or replacement
Serious incident cost$480,000 modeledRisingLowAbove risk appetiteFinance validates scenario

These values are illustrative, not benchmarks. A hospital billing interface, a warehouse label printer, and a public catalog need different boundaries. What matters is that the owners choose limits before the next incident and keep the evidence behind each value.

Do not collapse the four measures into one weighted score too early. A score of 62 hides whether the system has frequent cheap failures or one catastrophic unsupported dependency. Keep the four axes visible. If leadership wants a status, use three decision states:

  • Keep patching while the measures remain inside their boundaries and improve.
  • Contain and prepare when one measure crosses a boundary or several deteriorate.
  • Rewrite when exposure crosses the agreed limit and credible remediation has failed.

Set a review date and name the person who refreshes each input. Change failure data may come from deployment and incident records. Ownership evidence belongs with the engineering manager. Runtime support needs platform or security review. Finance and the business owner must sign off on incident consequence.

The sheet also prevents scope drift. If only a batch scheduler creates unsupported exposure, replace that component instead of declaring the whole estate rotten. If ownership is the only weak measure, rotation and documentation may solve it. A rewrite earns approval when the measures point to a system boundary that cannot be repaired economically in place.

Patching wins when the risk is bounded and reversible

Rewrite inside your perimeter
Supplied models can run air-gapped on customer-controlled hardware for regulated environments.

Patching is still the right answer when changes rarely fail, several people can own them, unsupported components have credible containment, and an incident would remain within the organization's tolerance. A rewrite consumes attention that could improve products customers actually notice. Do not replace stable software to satisfy an architecture diagram.

There are several strong patching cases. The system may be approaching a planned retirement because a business line is closing. Its behavior may be fixed by regulation or contract, with few expected changes. A vendor may offer supported upgrades that remove the exposed runtime without touching business logic. Or the application may sit behind a narrow, controlled interface with no untrusted input and a tested recovery procedure.

Patching should have an explicit scope and exit condition. Fund dependency upgrades, characterization tests, deployment automation, observability, and knowledge transfer. Then remeasure. If those changes lower failure rate and raise safe ownership, they are working. If the team spends each cycle rebuilding the same brittle deployment or shielding a runtime that blocks every other upgrade, the patching program has become an expensive delay.

I argue against the popular rule that every old monolith should first be split into microservices. That advice feels incremental, so it sounds safer than a rewrite. In practice, extracting services from code whose behavior nobody can specify often distributes uncertainty across a network. You add partial failures, versioned interfaces, and operational overhead before proving parity. First capture behavior at the system boundary; then choose boundaries that match business capabilities and data ownership.

Another valid option is selective replacement. Keep stable calculation or batch logic and replace the unsupported presentation layer. Move one database dependency behind a supported service. Retire reports that nobody uses before translating them. The four measures should be recalculated for the remaining boundary after every removal, because a smaller legacy core may become cheap enough to contain indefinitely.

Do not confuse patching with doing nothing. A decision to keep the system accepts specific work: support contracts, control testing, recovery drills, staffing coverage, and scheduled reevaluation. If nobody funds those items, the organization has not chosen patching. It has chosen unmanaged decline.

A rewrite must preserve behavior before changing architecture

A defensible rewrite captures observable behavior, runs old and new implementations against the same cases, and changes architecture only where the evidence supports it. Line-by-line translation preserves accidental structure and can reproduce defects without preserving the operational context that made them survivable.

Start with production-shaped evidence. Record requests and responses where policy allows, capture batch inputs and outputs, retain representative error cases, and identify side effects such as files, messages, database writes, and operator prompts. Scrub sensitive values, but keep the distributions, ordering, and malformed cases that exercise the real branches.

Build a parity harness that can send one case to both systems and compare normalized results. Normalize timestamps, generated identifiers, ordering where order has no meaning, and other nondeterministic fields explicitly. Never hide a difference by applying a broad text filter. Each normalization rule should state why the difference is irrelevant.

For stateful behavior, compare transitions rather than final screens. Seed both systems with the same database state, execute the action, and compare changed rows, emitted messages, files, and return values. For a batch job, test clean completion, restart after interruption, duplicate input, late input, and partial downstream failure. Operators often rely on those edge behaviors more than developers realize.

Architecture can then change safely. A COBOL and JCL estate may become Go services with a Postgres store; a numeric kernel may justify Rust; a desktop client may move to TypeScript. Those are design choices, not goals by themselves. The goal is equivalent business behavior with an execution model the current team can support.

CodeHero reads the whole source tree and checks the modernized system with a parity harness against recorded production traffic, rather than translating files in isolation. Its projects are delivered in under 30 days, including regulated deployments where supplied models can run air-gapped inside the customer's perimeter.

Treat parity failures as specification discoveries. Some reveal rewrite defects. Others expose contradictory old behavior, environment-dependent logic, or production data that violates the supposed rules. The business owner must decide which quirks are contracts and which are bugs. Engineering cannot infer that answer from code alone.

Approval requires a costed alternative, not rewrite enthusiasm

Test the awkward cases
Parity checks recorded traffic so restart paths and production quirks do not vanish during replacement.

A rewrite proposal should compete against a fully costed patching plan and a fully costed containment plan. If the alternative is "keep suffering," the comparison is rigged. Price the upgrades, tests, specialist coverage, support contracts, controls, incident exposure, and feature delay that patching actually requires.

Cost the rewrite beyond implementation. Include discovery, behavior capture, data conversion, parallel operation, user acceptance, cutover, rollback preparation, training, and decommissioning. Assign owners to business decisions and data validation. A technically complete replacement can still fail if finance cannot reconcile opening balances or operators cannot recover a half-finished batch.

Require evidence for schedule claims. Inventory integrations, data stores, scheduled jobs, reports, user roles, external file exchanges, and operational procedures. Mark each as observed, inferred, or unknown. Unknowns do not automatically block the project, but someone must decide how they will be tested before cutover.

The approval gate should answer five questions in plain language:

  1. Which of the four measures crossed its agreed boundary, and what is the evidence?
  2. What remediation did the team try, and how did the measure respond?
  3. What system boundary will be replaced, retained, or retired?
  4. How will the team prove behavioral parity and rehearse rollback?
  5. Which business owner accepts the remaining differences and cutover risk?

If those answers are vague, fund a short evidence phase instead of a rewrite. That phase should produce the runtime inventory, ownership assessment, incident model, behavioral corpus, and system boundary. It should not produce a slide deck about modernity.

Approval should also include stop conditions. If parity gaps remain in a high-consequence workflow, if data conversion cannot reconcile, or if the replacement cannot meet an operational recovery requirement, cutover waits. Sunk cost does not make an unverified system safe.

Recheck the decision after every material change

The rewrite decision expires whenever the system, team, or business consequence changes. Recalculate the four measures after a major dependency upgrade, ownership transfer, acquisition, traffic shift, new regulatory obligation, or serious incident. A decision sheet left untouched for a year becomes another piece of legacy documentation for everyone.

Keep the evidence close to normal engineering work. Deployment records should identify remediated changes. Incident reviews should separate change-caused failures from unrelated faults and retain actual labor and reconciliation costs. Skills evidence should come from delivered work. Runtime inventory should refresh from executing environments, not a yearly questionnaire.

Watch direction as well as thresholds. Four measures just inside their limits but deteriorating together justify preparation. One measure outside its limit but improving rapidly may justify another patching cycle. Write the exception, its owner, and its expiry date on the sheet so temporary tolerance does not quietly become permanent policy.

The hardest cases have conflicting signals. A system can be stable and cheap during incidents while ownership collapses. It can have many capable maintainers but depend on an unsupported public runtime. Do not average away the uncomfortable axis. Decide whether a credible control can move that measure before the hidden deadline arrives.

The first useful meeting is not a rewrite workshop. It is a review where engineering, operations, security, finance, and the business owner bring evidence for one number each. At the end, set boundaries and fund the next action tied to them. If the numbers support patching, patch without apology. If they show that every safe change depends on shrinking knowledge, abandoned software, and unacceptable incident cost, stop paying for the illusion that another patch will restore control.

FAQ

When should a company rewrite legacy software?

Rewrite when measured change risk and business exposure cross limits the organization agreed in advance, and focused remediation has failed. Age and unpopular technology alone do not justify replacement.

What is a good change failure rate for a legacy system?

There is no universal rate that fits every release process. Set a boundary using your own change volume, recovery time, and business consequence, then judge the trend across several review periods.

How do you measure who can safely maintain legacy code?

Count engineers who can independently explain, change, test, deploy, and recover a meaningful production change. Repository access, self-ratings, and work performed under the original expert's direction do not qualify.

Does an unsupported runtime always require a rewrite?

No. Isolation, strict input controls, virtual patching, or paid extended support can make patching rational. Replacement becomes urgent when the old component accepts untrusted input, blocks surrounding upgrades, or costs too much to contain.

How should incident cost be estimated before an outage?

Define a specific failure scenario and have finance, operations, security, and the business owner challenge each assumption. Include lost margin, loaded staff time, customer correction, contractual costs, reconciliation, and delayed business events.

Is low deployment frequency proof that legacy software is stable?

No. A system can look stable because the team is afraid to release it. Track postponed change demand beside change failure rate so operational paralysis does not masquerade as reliability.

Should a legacy monolith be split into microservices first?

Usually not before its behavior is captured. Splitting poorly understood code adds network failures and interface versioning while spreading the same uncertainty across more deployable units.

Can only part of a legacy system be replaced?

Yes, and selective replacement is often the soundest option. Remove the component that drives unsupported exposure or release failures, then recalculate the four measures for the smaller legacy boundary.

How do you prove a rewrite matches the old system?

Run old and new implementations against the same production-shaped cases and compare outputs and state transitions. Every normalization for timestamps, identifiers, or ordering needs a documented reason so real differences stay visible.

How often should the rewrite decision be reviewed?

Review it after any material change in dependencies, ownership, business consequence, traffic, or regulatory obligations. Also set a regular review date and give each of the four measurements a named owner.