Skip to main content
SEMastery
Fundamentalsbeginner

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.

12 min readUpdated March 25, 2026

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.

Old code carries hidden history that you must respect before changing it

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

Stop adding value
Old system still changes
New system chases it
Money runs out
Project cancelled

Steps

1

Stop adding value

Team focuses only on rewrite

2

Old system still changes

Bugs and new needs appear

3

New system chases it

Build everything twice

4

Money runs out

No business value shipped

5

Project cancelled

Both systems now a mess

Each step makes the gap between old and new grow wider

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.

ApproachRiskBusiness value during workEasy to undo?
Big-bang rewriteVery highNone until the endNo
Incremental rewriteLowerShipped continuouslyYes, step by step
Just refactor in placeLowShipped continuouslyYes

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.

A router sends most traffic to the old system and a growing share to the new one

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

Understand
Write tests
Change a little
Run tests
Repeat

Steps

1

Understand

Read code and ask users

2

Write tests

Capture current behavior

3

Change a little

One small step

4

Run tests

Confirm nothing broke

5

Repeat

Take the next small step

Tests first, then small changes, then repeat

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 worldModern .NET 10 world
Older .NET Framework, Windows onlyCross-platform, runs on Linux too
Slower, heavier on memoryFaster, lighter, cheaper to host
Verbose older C#Clean C# 14, less code to break
Out of support over timeLTS 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.

A rewrite touches many groups of people, and trust connects them all

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

Related Posts