What Rewriting a 40-Year-Old Project Taught Me About Software Development
Honest lessons from rewriting a 40-year-old legacy app to .NET 10. Why big-bang rewrites fail, the strangler pattern, tests, and respecting old code.
A program older than me
Last year I was handed a project that was 40 years old. Let that sink in. It was started before I was born. It had been written, fixed, and patched by many people across four decades. Some of them had retired. Some had passed away. And now it was my turn to bring it into the modern world, onto .NET 10.
I was excited. I was also a little scared. I thought I would just read the old code, write a fresh new version, and be done in a few months. I was very wrong. That project taught me more about software than any course or book ever did. This post is the honest story of what I learned, written so a beginner can follow every step.
A real-life example: the old family house
Think about a very old family house. Maybe your grandparents built it 40 years ago. Over the years, people added a new room, fixed a leaking roof, changed the wiring, and painted the walls many times.
Now imagine you want a modern house. You have two choices.
The first choice is to knock the whole house down and build a brand new one from nothing. It sounds clean and simple. But where will the family live while you build? What if the new house takes three years and you run out of money halfway? What if you forget that the old house had a hidden water tank that everyone depended on?
The second choice is to fix the house one room at a time. You modernize the kitchen this month. Next month, the bathroom. The family keeps living there the whole time. If one repair goes wrong, only that room is affected, not the whole home.
In software, the first choice is called a "big-bang rewrite." The second choice is called an "incremental rewrite." My biggest lesson was simple: the second choice almost always wins.
Lesson 1: The old code is smarter than it looks
When I first opened the 40-year-old code, I laughed. It looked messy. There were strange lines everywhere. One function had a check that made no sense to me at all. I thought, "These old developers did not know what they were doing."
I was being silly and rude.
That strange check was there because of a real bug that happened in 1998. A customer in one region had data in a slightly different shape, and the system crashed. Someone added that check to fix it. The fix worked. The reason was never written down, but the code remembered.
Old code tells you the what, but it rarely tells you the why. Your job is to find the why before you delete anything.
There is a famous idea called Chesterton's Fence. It goes like this: if you find a fence in a field and you do not know why it is there, do not remove it. First find out why someone built it. Only when you understand the reason are you allowed to take it down. Legacy code is full of these fences.
Lesson 2: "Make it do what it did before" is a trap
When the rewrite started, my boss said something that sounded easy: "Just make the new system do exactly what the old one does."
That sentence hides a giant trap. Nobody actually knows everything the old system does. The business rules were spread across the code, across the heads of long-time users, and across emails from years ago. There was no single document that listed them all.
Here is a small example of a rule that looked simple but was not.
// Looks like a plain discount rule...
public decimal ApplyDiscount(decimal price, Customer customer)
{
if (customer.IsLoyal)
return price * 0.90m; // 10% off
return price;
}But the real rule, hidden in the old code, was much messier. Loyal customers got 10 percent off, unless it was a public holiday, unless the item was already on sale, unless the customer was in a special region added in 2009. None of that was written down.
// The rule the business actually depended on
public decimal ApplyDiscount(decimal price, Customer customer, DateOnly today)
{
if (!customer.IsLoyal)
return price;
if (IsPublicHoliday(today))
return price; // no loyalty discount on holidays
if (customer.RegionCode == "RX09")
return price * 0.85m; // special 2009 region rule
return price * 0.90m; // normal 10% off
}The lesson: spend real time discovering the true rules up front. Talk to the people who use the software every day. They will often say, "Oh, everyone knows that," about a rule that is written nowhere. Each rule you discover early saves you huge pain later.
Lesson 3: Big-bang rewrites usually fail
This is the hardest truth I learned, and it is backed by real numbers. Reports from groups like the Standish Group and Gartner suggest that more than 70 percent of large legacy rewrites do not succeed. There are famous, painful examples where rewrites cost millions and still had to be thrown away.
Why do they fail so often? Here is the trap drawn out.
Why big-bang rewrites fail
Steps
Stop adding value
Team focuses only on rewrite
Old system still changes
Bugs and new needs appear
New system chases it
Build everything twice
Money runs out
No business value shipped
Project cancelled
Both systems now a mess
The biggest problem is the "moving target." While you build the shiny new system, the old one does not freeze. The business keeps finding bugs and asking for new features. So you have to build everything twice: once in the old code to keep the business alive, and once in the new code you are writing. You are chasing a target that keeps running away.
| Approach | Risk | Business value during work | Easy to undo? |
|---|---|---|---|
| Big-bang rewrite | Very high | None until the end | No |
| Incremental rewrite | Lower | Shipped continuously | Yes, step by step |
| Just refactor in place | Low | Shipped continuously | Yes |
Lesson 4: The strangler pattern saved us
After I learned all this, we changed our plan. We stopped trying to build a new system in one go. Instead, we used something called the strangler fig pattern.
The name comes from a real plant. A strangler fig grows around an old tree. Slowly, over years, it surrounds the tree completely. The old tree dies away, and the fig now stands in its exact shape. In software, the new system slowly grows around the old one, replacing it piece by piece, until the old one can be removed.
Here is how it works. You put a small router in front of the old system. At first, the router sends every request to the old code. Then, one feature at a time, you build the new version and tell the router to send just that feature to the new code. The user never notices. They keep using the same app.
The beauty of this is that each step is small and safe. If the new feature breaks, you flip the router back to the old code in seconds. The business never stops. You ship real value every single week instead of waiting years.
This is also the path Microsoft recommends for modernizing large apps. You do not jump everything at once. You move slowly and keep proving that the new pieces work.
Lesson 5: Tests are your safety net
Before I changed a single line, I learned to write tests that lock in the current behavior. These are called characterization tests. They do not ask, "Is this behavior correct?" They simply ask, "What does the system do right now?" and write it down as a test.
[Fact]
public void Loyal_customer_gets_ten_percent_off_on_normal_day()
{
var customer = new Customer { IsLoyal = true, RegionCode = "AB01" };
var today = new DateOnly(2026, 6, 10); // a normal day
decimal result = _pricing.ApplyDiscount(100m, customer, today);
Assert.Equal(90m, result); // locks in today's behavior
}Why is this so powerful? Because now, when I rewrite that pricing code in modern C# 14, I can run the test. If it still passes, I know I did not break the rule. If it fails, I caught my mistake in seconds instead of months later when an angry customer calls.
The wise order of work looks like this.
Safe order for changing legacy code
Steps
Understand
Read code and ask users
Write tests
Capture current behavior
Change a little
One small step
Run tests
Confirm nothing broke
Repeat
Take the next small step
Lesson 6: Moving to modern .NET is worth it
Once we had the strangler router and a good set of tests, we started moving real pieces onto .NET 10, which is the current Long-Term Support (LTS) release. LTS means it gets official support and security fixes for three years, so it is a stable target to aim for.
Modern .NET gave us real, measurable wins. Teams often report large performance gains and lower memory use after moving off old .NET Framework code. Newer C# (C# 14 shipped with .NET 10) also lets you write the same logic with far less noise, which makes the code easier to read and safer to change.
Here is a tiny taste of how much cleaner modern C# can be.
// Modern, clean C# 14 service in .NET 10
public sealed class PricingService(IHolidayCalendar calendar)
{
public decimal ApplyDiscount(decimal price, Customer customer, DateOnly today) =>
(customer, calendar.IsHoliday(today)) switch
{
{ Item1.IsLoyal: false } => price,
{ Item2: true } => price,
_ => price * 0.90m
};
}A quick word of caution that I learned the hard way: when you pick libraries for a long-lived project, check the license. Some popular .NET libraries that used to be free, like MediatR and MassTransit, have moved to a commercial license for many uses. That is fine, but you must know it before you build your whole system on top of them, so there are no money surprises later.
| Old world | Modern .NET 10 world |
|---|---|
| Older .NET Framework, Windows only | Cross-platform, runs on Linux too |
| Slower, heavier on memory | Faster, lighter, cheaper to host |
| Verbose older C# | Clean C# 14, less code to break |
| Out of support over time | LTS support and security fixes |
Lesson 7: People matter more than code
The last lesson surprised me the most. The hardest part of the rewrite was not the code. It was the people.
The users had trusted the old system for decades. They were nervous about change. The old developers felt protective of their work. My job was not just to write code; it was to listen, to explain, and to build trust. When I treated the old system and its people with respect, everyone helped me. When I acted like a clever new kid who knew better, doors closed.
Software is built by humans, for humans. A rewrite is a people project wearing a code costume.
Quick recap
- A 40-year-old project is full of hidden wisdom; respect it before you change it.
- Old code shows the what, not the why. Find the why first (Chesterton's Fence).
- "Just do what it did before" hides rules nobody wrote down. Discover them early.
- Big-bang rewrites fail more than 70 percent of the time. Avoid them.
- Use the strangler pattern: replace the old system one small, safe piece at a time.
- Write characterization tests first so you know the moment you break something.
- Move to .NET 10 (LTS) and C# 14 for speed, lower cost, and cleaner code.
- Check library licenses early; MediatR and MassTransit are now commercial for many uses.
- People and trust matter as much as the code itself.
References and further reading
- Upgrade .NET apps overview (Microsoft Learn)
- Strangler Fig pattern (Microsoft Learn)
- Lessons from 6 software rewrite stories (Herb Caudill)
- A Five-Step Process for Planning a Rewrite of a Legacy Project (Exception Not Found)
- Working on Legacy Software: Rewriting techniques and lessons (DEV Community)
Related Posts
How to Start a New .NET Project in 2026: A Beginner's Friendly Guide
A warm, step-by-step guide to starting a new .NET 10 project in 2026 with the dotnet CLI, the right template, and good folder habits from day one.
Best Practices for Increasing Code Quality in .NET Projects
Beginner-friendly guide to raising code quality in .NET with analyzers, EditorConfig, nullable types, tests, and CI checks that catch bugs early.
The Urge to Build Something: Turn That Spark Into a Real .NET App
Got the itch to build something? Learn how to turn that urge into a real, finished .NET 10 app with small steps, a tiny first project, and zero fear.
6 Steps for Setting Up a New .NET Project the Right Way
A friendly, step-by-step guide to starting a clean .NET 10 project with the right folder layout, central packages, analyzers, EditorConfig, and CI.
Building Semantic Search With Amazon S3 Vectors and Semantic Kernel
A beginner-friendly guide to building semantic search in .NET using Amazon S3 Vectors for cheap storage and Semantic Kernel for embeddings.
What Is Vector Search? A Concise Guide for .NET Developers
A simple, friendly guide to vector search for .NET developers: embeddings, similarity, nearest neighbors, and how to build it with Microsoft.Extensions.VectorData.