Is it better to rewrite or refactor legacy software?
In the Treviso case (220,000 lines, 500 days of changes a year) refactoring costs 91,728 euros and the rewrite, with freeze and risk, 190,680 euros more than its nominal cost. At five years the rewrite costs 1,642,880 euros against 1,671,103 for refactoring.

A window and door manufacturer in Treviso, 62 employees, 24 million euros in revenue, has used an order management system written in C# for twelve years: a WPF client, WCF services and a SQL Server database with 190 stored procedures. It is 220,000 lines on .NET Framework 4.7. Every change takes 70% longer than it would on a modern application, and the only developer who knows all the code has said that in two years he wants to do something else.
The owner has heard two voices. A supplier's consultant says to rewrite everything, because "you cannot build on a base like this". The in-house developer says to refactor, because "the code works and all the rules are in there". Both are right, and the argument never ends, because it is fought over taste and not over a number.
The number exists, and it is less technical than you would expect: the years of life the software has left. If the system has to live less than a certain threshold, refactoring costs less. If it has to live longer, rewriting costs less. In the Treviso case the threshold is 4.6 years, and this article explains where it comes from, how it moves and how to measure it.
You will find the cost in euros of the three roads (do nothing, refactor, rewrite), the measure that says how expensive it is to refactor your code, the assumptions that move the result most, and a way of working on the hot code without breaking anything. You will also find the C# model to redo the calculation with your own data.
An honest preface. Since 1999 I have been modernizing systems that have been in production for fifteen years, and I built and sold a multi-tenant legal management system. The Treviso case is composite, not a real client, but the ratios between the figures are the ones I find every time. The assumptions are written down one by one, so you can replace them with your own.
What do "refactor" and "rewrite" really mean?
The two words are used as if they were clear, and they are not. If two people in a meeting mean different things, the quote that comes out compares nothing.
Refactoring means changing the structure of the code without changing what it does. Martin Fowler defines it this way in his book on the subject: a series of small steps that preserve behavior, each one checked by tests. The system stays in production the whole time and users notice nothing. It is not a project with a delivery date: it is continuous work, done together with the changes the business asks for.
Rewriting means building a new system that replaces the old one. There are two variants here that cost very differently. The "big bang" rewrite replaces everything in one day, after one or two years of parallel development. The "module by module" rewrite, that of the Strangler Fig pattern, replaces one function at a time behind a facade. In this article "rewrite" means the first, because the second already has its own cost calculation, and we will meet it again in the last question of the decision tree.
There is also a third item that almost no quote writes down: doing nothing. It is not a neutral option. It costs every month, because every change pays the legacy tax. It is the starting point of any comparison, and without it the comparison is rigged.
| Road | What changes | When the benefit arrives | What is at risk |
|---|---|---|---|
| Do nothing | Nothing | Never | The legacy tax every month, and the key person leaving |
| Refactor | The structure of the code that is touched most | After 2-3 months, then continuously | Little: every step can be undone. But the system stays on an old platform |
| Big bang rewrite | Everything, at once | At the end, after 18-24 months | A lot: change freeze, lost rules, a project that stops |
Between the lines of the table is the reason why the comparison cannot be made by feel. Refactoring costs little and pays little at a time. Rewriting costs a lot and pays a lot, but only if it reaches the end. Which one wins depends on how long you will enjoy the benefit, and that is the number this article is about.
Why is the question "rewrite or refactor" asked wrongly?
When someone in a meeting says "we have to rewrite", they are usually talking about the code. They say it is ugly, that nobody understands it, that the variables are called tmp2 and flag_ok. When someone else answers "just refactor", they are talking about the same code, and they say it can be cleaned up. The argument is about code quality, and code quality decides nothing by itself.
What decides is the flow of money over time. Each road has a starting cost and an annual tax on changes. Ugly code matters only because it raises the tax. A horrible module that nobody has touched for five years costs nothing, and redoing it would be pure expense.
In 2000 Joel Spolsky wrote a famous article in which he called the from-scratch rewrite the worst strategic mistake a software company can make, and used Netscape as the example, which left the field to its competitors for about three years while redoing its browser. He is right about the risk, and right that old code holds knowledge. He has no number that says when a rewrite does pay, and that number exists: it depends on how long the system lives.
Michael Feathers, in his book on legacy code, gives a definition I have found very useful: legacy is code without tests. Not old code, not ugly code, but code you cannot change with confidence. If you accept the definition, the question changes. It no longer asks "do we rewrite it?", it asks "how much does it cost to bring this code to a state where I can change it with confidence, and how long does the benefit last?".
There are three questions, and they go in this order.
- For how many years will I keep this software alive? If the answer is "I don't know", the rest is useless: the first measure is to write down a date and a reason.
- How much of the logic that is touched most can be tested without opening a form and without a database? The answer decides how much it costs to refactor.
- Does the platform still hold? If the system runs on something that no longer receives security updates or cannot be installed on a new server, neither cleaning nor rewriting the code solves the problem: the base has to be moved first.
We turn the first two into numbers in the next sections. The third is a yes or a no, and if it is a no the path changes, as the decision tree near the end shows. First, though, we need the case.
What are the assumptions of the Treviso case?
To compare three roads you need explicit assumptions. I put them all here, in a table, because they are the most debatable part of the calculation and I want you to be able to challenge them one by one.
| Assumption | Value | How I chose it |
|---|---|---|
| System size | 220,000 lines of C#, T-SQL and XAML | An order management system of a manufacturing SME after twelve years |
| Changes requested each year | 500 days, measured at "modern" speed | About two people full time on fixes and enhancements |
| Average rate | 420 euros per day | Mix of employees and contractors |
| Legacy multiplier | 1.7 | Every change costs 70% more, for orientation, regressions and manual testing |
| Infrastructure | 14,000 euros a year on the old system, 9,000 on the new one | Windows servers, licenses, backup |
| Hot code | 30% of the lines receive 75% of the changes | A rule I find almost everywhere. It is measured from Git history |
| Days per 1,000 lines, isolable logic | 1.0 | Only writing characterization tests: the code is already separated |
| Days per 1,000 lines, entangled logic | 6.0 | First the tests are written, then the logic is extracted from forms and stored procedures |
| Refactoring start-up | 60 days | Build pipeline, system tests, initial measurement |
| Hot code multiplier afterwards | 1.2 | Cleaned code still costs a little more than new code: the platform stays old |
| Rewrite | 3.5 days per 1,000 lines, that is 770 days | New system, no code reuse, with learning of the rules |
| Freeze during the rewrite | 40% of changes are not made for 18 months | The rest is done on the old system, at full cost |
| Probability that the rewrite stops | 25% | Professional judgment, discussed in the robustness section |
Two assumptions deserve a word. The first is the 30% of lines that receives 75% of the changes. In the Treviso system it means 66,000 hot lines out of 220,000. This concentration is the reason refactoring costs little: you do not clean all the code, you clean the code where the work happens. If in your system the changes are spread evenly, the calculation changes, and that is why the article on the Strangler Fig starts from that very measure.
The second is the difference between 1.0 and 6.0 days per 1,000 lines. It is the central insight of the article: extracting logic from a form or a stored procedure costs six times more than putting it under test when it already sits in a separate class. At 3.5 days per 1,000 lines the rewrite sits in the middle, and that is why the share of isolable code moves the result.
The rest are costs anyone can check against their own accounts. The calculation starts here.
How much does doing nothing cost?
The starting point is the annual cost of the system as it is today. The extended method is in the guide to the cost of legacy software. Here I use the short form.
500 days × 1.7 × 420 euros = 357,000 euros, plus 14,000 of infrastructure, that is 371,000 euros a year. Over five years that is 1,855,000 euros, over eight 2,968,000.
Before commenting on the number, a clarification that avoids a common misunderstanding: it is not money thrown away. Five hundred days of changes would be needed even with a new system, because the business changes. What a better system removes is the difference between 1.7 and 1.0, that is 147,000 euros a year of friction, plus 5,000 of infrastructure. That is the treasure both roads try to recover.
The second misunderstanding is treating the bill as fixed. It is not, for three reasons that point in the same direction.
- The key person. The in-house developer who knows everything and wants to leave is not an abstract risk. When he goes, the multiplier rises because nobody knows where to put their hands, and the friction goes from 1.7 to 2.2 in a few months. With 500 days a year that is 105,000 euros more.
- The platform. .NET Framework 4.7 still receives security fixes inside Windows, but every third-party component that stops supporting it closes a door. The bill does not include the day a UI control no longer installs.
- The business. The requests get harder to satisfy: integration with a portal, digital signature, a new e-invoicing format. Each time an integration is paid for "by hand" that a modern architecture would give almost free.
So 371,000 euros a year is the most optimistic estimate of the cost of doing nothing: the three phenomena only push it up. If the estimate looks high to you, I suggest doing the exercise with your own numbers before going on, because every later figure is compared with this one.
A legacy system does not cost because it is old. It costs because every change pays a 70% tax, and pays it every time.
How much does refactoring cost, and what does it depend on?
Here comes the number that moves the whole calculation: the share of hot code whose business logic is isolable. I call isolable a class that can be tested with an automated test without opening a form, without a database connection, without the context of a web page. Its opposite is entangled logic: the discount calculation inside a button click, the pricing rule inside a 600-line stored procedure, the validation done with a query inside the code-behind.
Why does it matter? Because to refactor safely you need tests, and to write tests you must be able to call the code from outside. If the logic is already in a class, writing characterization tests is cheap: in the model 1.0 days per 1,000 lines. If it is entangled, it must first be extracted without breaking anything, and the first extraction is done without a net: in the model 6.0 days per 1,000 lines.
In the Treviso system I measured that 72% of the hot lines are isolable. The calculation goes like this.
- Hot lines: 66,000 (30% of 220,000).
- Days: 60 for start-up, plus 66 × (0.72 × 1.0 + 0.28 × 6.0) = 66 × 2.4 = 158.4. Total 218.4 days.
- Cost: 218.4 × 420 = 91,728 euros, spread 60% in the first year and 40% in the second.
- Effect: changes on the hot code go from 1.7 to 1.2. 75% of changes cost 29% less, 25% stay as they are. The average multiplier falls from 1.7 to 1.325.
The annual cost goes from 371,000 to 292,250 euros, a saving of 78,750 euros a year at steady state. The investment pays back in a little over a year.

If instead the share were 20%, as in the second case in the chart, the days would be 60 + 66 × (0.2 × 1.0 + 0.8 × 6.0) = 60 + 330 = 390, for a cost of 163,800 euros. Almost double, for the same result. And while the starting cost rises, the steady-state benefit does not change, so the crossover with the rewrite arrives sooner. We will see it shortly.
How to measure the isolable share in an afternoon
The method is rough, but the order of magnitude is what you need. Take the files touched in the last twelve months, add up their lines, and separate those that by path or by content are entangled: code-behind, XAML.cs files, forms, views, SQL, and any file that opens a connection or shows a window.
git log --since="12 months ago" --name-only --pretty=format: -- '*.cs' '*.sql' \
| sort -u | grep -v '^$' > hot.txt
tot=0; ent=0
while read -r f; do
[ -f "$f" ] || continue
lines=$(wc -l < "$f")
tot=$((tot + lines))
if [[ "$f" == *.xaml.cs || "$f" == *.aspx.cs || "$f" == *.Designer.cs \
|| "$f" == *.sql ]] \
|| grep -qE 'SqlConnection|MessageBox|HttpContext' "$f"; then
ent=$((ent + lines))
fi
done < hot.txt
echo "hot lines: $tot, entangled: $ent, isolable: $((100 * (tot - ent) / tot))%"The result is an underestimate of the isolable share, because a class containing a single database call is counted entirely as entangled. For a decision worth hundreds of thousands of euros you refine it by hand on the ten largest files, but the distance between 72% and 20% shows at once, and it changes the road.
How much does rewriting cost, with the freeze and the risk?
Now the other road. The big bang rewrite has a nominal cost and two costs that quotes do not write down.
The nominal cost. 220,000 lines at 3.5 days per 1,000 are 770 days, that is 323,400 euros. The days per line are fewer than for refactoring entangled code (6.0), because in a new system there is nothing to untangle. But learning the business rules, acceptance tests with users and data migration all count, and they are already included in the 3.5 days.
The spending is spread over eighteen months: two thirds in the first year, one third in the second. During that period the old system is still paid for, with 14,000 euros a year of infrastructure, plus 20,000 euros of parallel running in the second year, when the two systems coexist.
The change freeze. For eighteen months the team works on the new system, and not all the requested changes can be made to the old one. In the model 40% of those requests are not done: that is 500 × 0.4 × 1.5 years = 300 days, which at 420 euros are worth 126,000 euros of work the business wanted and did not get. It is a prudent estimate, because I counted only the development cost and not the benefit those changes would have brought.
The risk. With a 25% probability that the project stops before the finish line, and with 80% of the money spent up to that point not recoverable, the expected risk is 0.25 × 323,400 × 0.8 = 64,680 euros. It is not a cost you will certainly pay: it is the price of an insurance policy nobody sells you, and that is why it goes in the bill.
The steady-state cost, once the new system is in production, is the lowest of the three roads: 500 days at 1.0 times 420 euros, plus 9,000 of infrastructure, that is 219,000 euros a year. Every year it saves 152,000 euros compared with doing nothing, against 78,750 for refactoring. That is why, in the long run, the rewrite wins. Its problem is getting there.
| Item | Amount in euros |
|---|---|
| Nominal cost of the rewrite (770 days) | 323,400 |
| Changes not made for 18 months | 126,000 |
| Expected risk of stopping | 64,680 |
| Extra spending over the nominal cost | 190,680 |
| Annual steady-state cost | 219,000 |
The extra 190,680 euros are 59% of the declared cost of the rewrite. If your quote gives it as "323,000 euros, all included", read the numbers below: it probably includes neither the freeze nor the risk.
What is the deciding number: the years of remaining life
We have the three roads. Now we only need to line up the costs year by year. In the Treviso case, with 72% isolable logic, these are the annual costs.
| Year | Do nothing | Refactor | Rewrite (direct costs only) |
|---|---|---|---|
| 1 | 371,000 | 426,037 | 443,800 |
| 2 | 371,000 | 368,316 | 351,400 |
| 3 | 371,000 | 292,250 | 219,000 |
| 4 | 371,000 | 292,250 | 219,000 |
| 5 | 371,000 | 292,250 | 219,000 |
Two things stand out. In the first year, refactoring costs 55,037 euros more than doing nothing, and rewriting 72,800 more. From the third year the rewrite costs 73,250 euros less than refactoring, every year. The reason is the multiplier: at steady state it is 1.0 for the rewrite and 1.325 for refactoring.
The table above does not say everything yet, because the rewrite is missing the 190,680 euros of freeze and risk. I add them in the table that matters, the one of cumulative costs, where each row answers the question "if I keep the software alive for N years, how much do I spend in total?".
| Years of life | Do nothing | Refactor | Rewrite, with freeze and risk | Cheapest road |
|---|---|---|---|---|
| 3 | 1,113,000 | 1,086,603 | 1,204,880 | Refactor |
| 4 | 1,484,000 | 1,378,853 | 1,423,880 | Refactor |
| 5 | 1,855,000 | 1,671,103 | 1,642,880 | Rewrite |
| 6 | 2,226,000 | 1,963,353 | 1,861,880 | Rewrite |
| 8 | 2,968,000 | 2,547,853 | 2,299,880 | Rewrite |

The table says one clear thing. At four years the rewrite costs 45,027 euros more than refactoring. At five years it costs 28,221 euros less. The crossover, by interpolation, is at 4.6 years. Below that threshold it pays to refactor the hot code, above it it pays to rewrite. At eight years the advantage of the rewrite is 247,973 euros.
There is a second, lower threshold. Refactoring beats doing nothing only from 2.7 years onward. If the software has to live less than that, in this configuration the investment does not pay back, and the right road is to leave it alone and take care of the minimum: backups, encryption keys and tests on the parts that break.
The two thresholds divide time into three zones, and each has a road.
- Less than about 3 years of life: leave it alone. The starting cost does not pay back.
- From 3 to 4.6 years: refactor the hot code. It is the cheapest road and also the safest, because every step can be undone.
- More than 4.6 years: rewrite, preferably module by module. At that point the 70% tax weighs more than the cost of getting rid of it.
The deciding number, then, is not technical. It is a date. The question to ask the owner is not "is the code ugly?", it is "until when do you need this software?". If the answer is "as long as the company exists", the zone is the third. If it is "until we change management system in three years", it is the first. What I see most often, though, is that nobody has an answer, and the software has lived for twelve years without anyone deciding how long it should live.
How does the threshold change when the isolable share changes?
So far we have looked at the Treviso case, with 72% isolable code. The share moves the threshold because it changes the cost of refactoring, while the rewrite costs the same. Here is the threshold for five share values.
| Isolable hot code | Refactoring days | Cost in euros | Crossover with the rewrite | Threshold against doing nothing |
|---|---|---|---|---|
| 20% | 390 | 163,800 | 3.6 years | 3.6 years |
| 40% | 324 | 136,080 | 4.0 years | 3.2 years |
| 60% | 258 | 108,360 | 4.4 years | 2.9 years |
| 72% (Treviso) | 218 | 91,728 | 4.6 years | 2.7 years |
| 80% | 192 | 80,640 | 4.8 years | 2.5 years |

The result surprises anyone expecting a big effect. Going from 20% to 80% isolable code moves the crossover by a little over a year, from 3.6 to 4.8. What it moves much more is the starting cost, which falls from 163,800 to 80,640 euros, and the payback time. In other words, the isolable share does not decide whether to refactor, it decides how much it costs and how fast it pays back.
The practical reading is this. If you have a lot of entangled logic, refactoring costs a lot and pays only for short horizons, under 3.6 years. If you have a lot of isolable logic, refactoring is a small investment and pays back fast, but the rewrite still wins in the long run. The reason is the platform: clean code still sits on an old architecture, and the multiplier never falls below 1.2.
The isolable share has another effect, though, which the four-year model does not measure. The higher it is, the more the work is reversible and divisible. At 72% you can start with one module, see the results after a month and stop. At 20% the first month is spent almost entirely extracting code and writing tests, and to see the first benefit you have to trust.
When the share is under 25%, the cases I have followed split in two. In one part refactoring worked, provided it started from a single module and stopped at every step. In the other, most attempts stopped in the second month, because the apparent cost was too high and the benefit still invisible. If you are in that zone, the first measure is to pick a small module and try, before committing the whole budget.
How robust are these numbers?
A crossover at 4.6 years sounds precise, and it is not. It depends on assumptions I chose, and the honest thing is to show how much it moves if you change them. I repeated the calculation changing one assumption at a time, with the isolable share at 72%.
| Assumption changed | Base value | Alternative value | Crossover |
|---|---|---|---|
| Probability that the rewrite stops | 25% | 10% / 40% | 4.1 / 5.1 years |
| Days per 1,000 lines of rewrite | 3.5 | 2.5 / 5.0 | 3.1 / 6.9 years |
| Legacy multiplier | 1.7 | 1.4 / 2.0 | 6.1 / 3.6 years |
| Hot code multiplier after cleaning | 1.2 | 1.0 | 7.0 years |
| Base case | 4.6 years |
The range goes from 3.1 to 7.0 years. It is not small, and the most sensitive assumption is the cost of the rewrite per line. If your new system costs 5 days per 1,000 lines instead of 3.5, the crossover rises to 6.9 years and the rewrite almost never pays. It is the assumption on which quotes are least reliable, because a rewrite misestimates far more often than a cleaning job.
The second sensitivity point is the legacy multiplier. If your tax is 40% (1.4) instead of 70%, the friction to remove is smaller and the rewrite takes 6.1 years to pay back. If it is 100% (2.0), the crossover falls to 3.6 years. This means that before choosing you must measure your own multiplier: not with an opinion, but by comparing the time your developers estimate for a typical change with the time it would take in a clean project. I cover it further on.
The third is the quality of the refactoring result. If the cleaned code ended up costing as much as new code (1.0), the crossover would rise to 7.0 years: cleaning would almost always be enough. In practice it does not get there, because the old platform keeps imposing its rules, but the value 1.2 is an assumption you can verify after the first two months of work.
From this I draw a rule for using these numbers without fooling yourself. Do not look for the exact year: look for which of the three zones you fall in with margin. If the expected remaining life is two and a half years, you are well inside the first zone in every scenario. If it is ten, you are in the third in every scenario but one. If it is four, you are in the grey zone and another measure is worth it: a small, low-cost trial that reduces the uncertainty on the most sensitive assumptions.
How do you refactor the hot code without breaking anything?
Suppose the number tells you to refactor. The risk is doing what is usually done: opening the ugliest file and starting to tidy it. That breaks things that worked, and after two production incidents management says to stop. The right method has four steps, in order.
1. Measure the hot code and leave the rest alone
Use Git history, as in the script above, but sorting files by number of changes instead of adding up lines. The first twenty files are the perimeter. Everything else, for now, does not exist. The advantage is psychological as well as economic: a perimeter of twenty files is a goal people understand.
2. Write characterization tests before touching the code
A characterization test does not say the code is right: it says it does what it does today. You call the function with real data, record the result, and the test becomes the net. If after cleaning the result changes, something broke, or a hidden bug was fixed, and in both cases you want to know.
public class OrderPriceCalculationTests
{
[Theory]
[InlineData("SER-0412", 12, 18_450.00)]
[InlineData("SER-0987", 3, 4_320.50)]
[InlineData("SER-1530", 40, 61_800.00)]
public void The_price_stays_the_recorded_one(string item, int quantity, decimal expected)
{
var priceList = TestPriceList.FromFile("pricelist-2026-09.json");
var calculation = new OrderPriceCalculation(priceList);
decimal price = calculation.Calculate(item, quantity, CustomerWithHistoricDiscount.Rossi);
Assert.Equal(expected, price);
}
}The "expected" values do not come from a specification: they come from a run of the old system with the same data. If the number is wrong, it is wrong today as well, and the test keeps it until you decide to fix it. That is the difference between cleaning and rewriting from imagination.
3. Extract the entangled logic with the seam technique
Feathers calls it a "seam": a point in the code where behavior can be changed without editing it. The most common case is logic inside a button handler. You create a new class, move the calculation, and the handler calls its single method. The change is mechanical, and that is why it is done with the development environment's tools and not by hand.
The order of the extractions matters: start with pure calculations (prices, discounts, due dates, rounding), which do not touch the database, and finish with rules that read data. Stored procedures come last, and usually they are not translated: you write a test that calls them with known data and put them behind a class that encapsulates them.
4. Clean one module at a time, and stop after each module
After each module you measure again: average time of a change, number of regressions, days spent. If the multiplier has fallen, go on. If it has not, or it has fallen less than planned, stop and recompute the threshold with the real value. The nice thing about this road is that every module brings a measurement, and the measurement corrects the most sensitive assumption of the model.
In general, the first two modules take longer than expected, because you are building the equipment: the pipeline, the test data, the habit of testing. From the third the pace picks up. If after five modules the team is not at least 30% faster, it means the entangled logic is more entangled than you measured, and it is time to look at the other column of the table.
What if the problem is not the code but the platform?
So far I have assumed the system runs on a base that holds. Sometimes it does not, and then neither cleaning nor rewriting the code solves the urgency. There are three situations.
- The runtime no longer receives updates. An application on an unsupported framework, or on an operating system the company can no longer install, is a security risk even if the code is perfect. The typical case is an application that depends on a 32-bit component for which no updated version exists.
- The deployment model is outdated. A desktop program that has to run in ten offices with manual installs is a problem clean code does not solve. You need a web model or a client with automatic updates.
- The database is the obstacle. A 600-line stored procedure doing the application's work cannot be refactored safely, and sometimes the real choice is to move the logic out of the database, regardless of everything else.
In these cases the starting point changes. You migrate the platform first, piece by piece, with a facade and one module at a time: it is the work I describe in the guide to migrating from .NET Framework to .NET 10. If the program is a desktop application, the article on Windows Forms in 2026 shows the three choices, keep it, port it or rewrite it, with costs.

The decision tree holds together everything we have seen, in four questions and in this order.
- Does the platform hold for another four years? If not, migrate the platform first, piece by piece. The other questions come after.
- Does the software live less than three years? If so, leave it alone and take care of the minimum only.
- Does it live less than the crossover years, between 3.6 and 4.8 depending on the isolable share? If so, refactor the hot code.
- Otherwise, rewrite module by module. The big bang rewrite, with all its risks, makes sense almost only when the modules cannot be separated, and in that case it is worth understanding why before spending.
A note on the fourth line. The model compares the big bang rewrite, because it is the one proposed most often. In practice, when the answer is "rewrite", the right road is almost always the module-by-module variant, with the facade, because it removes almost all of the risk and the freeze, and I measured it in the article on the Strangler Fig. The big bang stays on the table only when the system is small or when there is no seam where the facade can be inserted.
How do you redo the calculation with your company's data?
The model fits in a few lines, and it is worth having it in executable form. It is plain C#, with no dependencies: paste it into a console application on .NET 10, replace the assumptions and read the totals. Here is the core. With the Treviso values it returns 1,671,103 euros for refactoring and 1,642,880 for the rewrite over five years.
var h = new Assumptions();
for (int years = 3; years <= 8; years++)
Console.WriteLine($"{years} years: refactor {Model.Refactor(h, years, 0.72).Sum():N0}, rewrite {Model.Rewrite(h, years):N0}");
record Assumptions(
double Rate = 420, double ChangeDays = 500, double Multiplier = 1.7,
double OldInfra = 14_000, double NewInfra = 9_000,
double Lines = 220_000, double HotLineShare = 0.30, double HotChangeShare = 0.75,
double HotAfter = 1.2, double DaysIsolable = 1.0, double DaysEntangled = 6.0, double StartUp = 60,
double RewriteDays1000 = 3.5, double Freeze = 0.40, double PFailure = 0.25);
static class Model
{
public static double[] Refactor(Assumptions h, int years, double q)
{
double days = h.StartUp + h.Lines * h.HotLineShare / 1000 * (q * h.DaysIsolable + (1 - q) * h.DaysEntangled);
double investment = days * h.Rate;
double after = h.HotChangeShare * h.HotAfter + (1 - h.HotChangeShare) * h.Multiplier;
var mult = new[] { h.Multiplier, (h.Multiplier + after) / 2 };
return Enumerable.Range(0, years).Select(i =>
h.ChangeDays * (i < 2 ? mult[i] : after) * h.Rate + h.OldInfra +
(i == 0 ? investment * 0.6 : i == 1 ? investment * 0.4 : 0)).ToArray();
}
public static double Rewrite(Assumptions h, int years)
{
double investment = h.Lines / 1000 * h.RewriteDays1000 * h.Rate;
double y1 = h.ChangeDays * (1 - h.Freeze) * h.Multiplier * h.Rate + h.OldInfra + investment * 2 / 3;
double y2 = h.ChangeDays * (0.5 * (1 - h.Freeze) * h.Multiplier + 0.5) * h.Rate
+ (h.OldInfra + h.NewInfra) / 2 + investment / 3 + 20_000;
double rest = (h.ChangeDays * h.Rate + h.NewInfra) * (years - 2);
double frozen = h.ChangeDays * h.Freeze * 1.5 * h.Rate;
double risk = h.PFailure * investment * 0.8;
return y1 + y2 + rest + frozen + risk;
}
}Three measures are needed to use it, and none requires a consultant. The days of changes you find in the ticket log or in the hours of the last twelve months. The isolable share you estimate with the script I showed. The legacy multiplier you get by having your developers estimate, cold, five changes already made, and comparing the real time with what they would have taken in a clean project: the ratio is your 1.7.
The rest are assumptions you can leave where they are, provided you look at the sensitivity table and do not trust a single scenario. A sheet with three scenarios, optimistic, base and pessimistic, is worth more than a precise number.
The most useful way to use the model is to run it three times, changing only two parameters. In the optimistic scenario for the rewrite put 2.5 days per 1,000 lines and a 10% probability of stopping. In the pessimistic one put 5.0 days and 40%. The base one stays as it is. If your remaining-life date falls in the same zone in all three scenarios, the decision is solid and there is no need to discuss it further. If it falls in different zones, the problem is not the model: it is that the rewrite is too uncertain to be chosen today, and the right step is a small trial that reduces the uncertainty.
One last practical piece of advice. Keep the file with the assumptions together with the code, in the repository, and redo the calculation every six months with real numbers: the measured multiplier, the days spent, the updated isolable share. A model that is corrected with data becomes a governance tool, while a quote written once remains a dated opinion.
Which mistakes have I seen most often?
After twenty-six years in the trade, the ways of getting this decision wrong are few and they repeat. I list them because they save you more than any formula.
Confusing annoyance with cost. The code the team hates is not for that reason the most expensive. The module nobody wants to open has often been still for years and generates no spending. I always look at the change history first and at opinions second, because opinions are noisy and commits are not.
Refactoring forever with no end date. Cleaning without measurement becomes a hobby. In the projects that worked there was a written goal: "after the third module, the average time of a change falls by 25%". When it did not, we said so, and changed road.
Rewriting without freezing the scope. The new system starts out replicating the old one, and after six months management asks for things the old one never did. You end up building a third system that is neither. A rewrite works when the scope of the first release is identical to that of the old system, with no surprises.
Underestimating the rules nobody remembers. In twelve years the code absorbs hundreds of special cases: the customer who pays at 75 days, the discount that does not apply to aluminum frames, the rounding the accountant wants at the third decimal. Nobody wrote them down. If you rewrite without characterization tests, you lose them, and you find out when a customer complains.
Not writing down the remaining-life date. It is the most serious mistake, because it makes the decision impossible. When I ask "for how long do you need this software?" in half the cases there is no answer, and the software is kept alive by inertia. Writing "at least seven years, because it is the heart of the company" changes everything, for the better.
Choosing based on who proposes. A supplier that sells rewrites proposes to rewrite. A consultant who sells cleanups proposes to refactor. I do both, and that is why my first question is always about the numbers and never about the solution. If whoever proposes a road does not show you the bill of the other, you have received an opinion.
How do you decide in one week?
The decision does not require a project. It requires five days of work by one or two people, and produces a one-page sheet with three numbers and a recommendation. The calendar I use is this.
- Day 1: the date. With management, write down for how many years the software has to live and why. Write three scenarios: minimum, probable, maximum. If no agreement is found, the difference between the scenarios is already information.
- Day 2: the hot code. Extract the Git history of the last twelve months, identify the files touched and compute the share that receives 75% of the changes. If it is very different from 30%, change the assumption in the model.
- Day 3: the isolable share. Run the script on the hot code and refine by hand on the ten largest files. The result is a number between 0 and 100.
- Day 4: the multiplier. Have two developers who do not talk to each other estimate, cold, five changes already made. The ratio between real time and clean estimate is the multiplier.
- Day 5: the model and the sheet. Enter the numbers in the model and read three things: which zone the date falls in, how much the crossover is worth, how much it changes in the pessimistic scenario. The sheet ends with a decision and a first low-cost step.
The first step is always small and reversible. If the zone is the second, it is one module to clean in four weeks, with the multiplier measured before and after. If it is the third, it is the facade and the first module to replace, as in the Strangler Fig case. If it is the first, it is a list of minimum things to secure and a date to repeat the measurement.
At the end of the week you have three things you did not have before: a written number of years, a share of isolable code and an annual cost of friction. With these three, the meeting with management changes subject: you no longer argue between rewriting and refactoring, you decide which road brings more value over the next years, and how much you are willing to spend on the first two months.
What would have happened in Treviso?
Back to the window manufacturer. The owner had two voices, the supplier who wanted to rewrite and the developer who wanted to clean up. Nobody had asked him for how many years he wanted to keep the system.
With the bill in hand he would have seen that doing nothing cost 371,000 euros a year, and that 72% of the hot code was isolable, so that refactoring cost 91,728 euros and that at four years of life it cost 45,027 euros less than a rewrite. He would have seen that the rewrite, with freeze and risk, cost 190,680 euros more than its nominal cost, and that the crossover was at 4.6 years.
The next question would have been the decisive one: how many years? If the answer had been "at least eight", he would have chosen to rewrite module by module, saving 247,973 euros over the period compared with cleaning. If it had been "three or four, then we change system", he would have cleaned the twenty hot files, spent 91,728 euros and saved 105,147 euros over four years compared with doing nothing.
In both cases he would have decided with a number and not with an argument. That is the difference, and it is worth more than the outcome.
If you have a similar system, written years ago by someone who is no longer there, and you want to know which zone yours falls in, the path starts with getting in touch. To work out the hidden costs there is the guide to the cost of legacy software, and if you need someone to look at the code with you, you can write to me through the contact page.
In five lines. Write down for how many years you will keep the software alive, because without a date there is no decision. Measure the share of isolable hot code, because it says how much cleaning costs. Under about three years leave it alone, up to 4.6 clean the hot code, beyond that rewrite module by module. Write characterization tests before moving a line. After each module measure again, and correct the model with the real number.
Frequently asked questions
It depends on how many years it still has to live. In the case described, under about 3 years it is best to leave it alone, up to 4.6 years it pays to refactor the hot code, beyond that it pays to rewrite. At five years the rewrite costs 1,642,880 euros against 1,671,103 for refactoring, and at eight years it saves 247,973 euros.
Changing the structure of the code without changing what it does, in small steps checked by tests. The system stays in production and users notice nothing. It has no delivery date: it is continuous work, done together with the changes the business asks for.
For a 220,000-line system, 770 days, that is 323,400 euros at 420 euros a day. But the real bill adds 126,000 euros of changes not made for 18 months and 64,680 euros of expected risk of stopping: 190,680 euros more, 59% of the declared cost.
It is business logic that can be tested with an automated test without opening a form and without a database. It matters because refactoring needs tests: with 72% of hot code isolable it takes 218 days and 91,728 euros, with 20% it takes 390 days and 163,800 euros.
With the Git history of the last twelve months: take the touched files, add up the lines and separate the entangled ones, meaning code-behind, XAML.cs, SQL and files that open connections or windows. The script in the article does it in an afternoon and gives an underestimate.
The 4.6-year crossover moves between 3.1 and 7.0 when one assumption at a time is changed. The most sensitive is the cost of the rewrite per line: at 5 days per 1,000 lines the crossover rises to 6.9 years. That is why you look for the zone you fall in with margin, not the exact year.
