Skip to content

Modernizing .NET Without the Big Rewrite

By Zane Rakhmonov7 min readRSS

The rewrite temptation

Every engineer who has inherited a legacy .NET system has felt the pull. The domain model is tangled, the dependency injection is non-existent, the tests are either absent or testing the wrong things, and every new feature requires navigating five layers of indirection that made sense in 2011 but have long since calcified.

The instinct is: "Let's start fresh." It feels clean. It feels like progress. It is almost always wrong.

Why rewrites fail

Rewrites fail for a predictable set of reasons that have nothing to do with engineering skill.

The system is more complex than it looks. Legacy systems contain years of encoded business logic, edge cases, and regulatory requirements. Some of it is documented. Most of it isn't. The engineers who wrote it may have left. The rewrite discovers this complexity gradually, usually in production.

The business can't pause. A rewrite takes time — often 12–24 months for a system of any real size. During that time, the business keeps running on the old system, which keeps accumulating changes. By the time the rewrite is "done," it's already out of date.

Two systems are twice the cost. Running parallel systems requires double the infrastructure, double the deployment overhead, and double the operational burden. You planned for one migration; you're running two production systems.

The alternative: incremental modernization

The approach we take is not to replace the system — it's to progressively improve it from the inside, one seam at a time.

Step 1: Understand before touching

Before writing any new code, we spend time understanding the system's actual behavior. This means reading the code, running it under load, and mapping the domain model as it exists — not as it should exist. We're looking for seams: places where the system's coupling is low enough that we can extract a module without pulling the whole thing apart.

Step 2: Build a strangler fig, not a replacement

The strangler fig pattern is the backbone of most successful modernizations we've done. New functionality goes in a new module — a separate service, a MAUI-based UI layer, a clean-room domain model. The old system continues to run. Traffic migrates to the new module as it stabilizes. Eventually, the old code is "strangled" — bypassed entirely — rather than replaced wholesale.

This keeps the system running throughout. There's no big-bang cutover.

Step 3: Upgrade the runtime incrementally

Before refactoring the domain, upgrade the framework. Get from .NET Framework 4.x to .NET 8. This is often less painful than teams expect — Microsoft's upgrade tooling has improved significantly — and it unlocks performance improvements, modern dependency injection, and the full modern C# language.

Step 4: Add tests at the seams, not everywhere

You won't achieve full coverage of a legacy system before modernizing. You don't need to. The goal is to add tests at the seams you're about to touch — integration tests that verify behavior at the boundary, not unit tests for every class. This gives you a safety net for the changes that matter without burning months on the changes that don't.

What "done" looks like

Done doesn't mean "rewritten." It means the system is:

  • Running on a supported runtime with a clear upgrade path
  • Structured so that new features can be added without touching the legacy core
  • Observable — logs, metrics, traces — so the team knows when something breaks
  • Understood by the current team, not just the people who built it

That's a system that can serve the business for another decade. Which is usually the goal.

If you're evaluating a .NET modernization, start with our assessment engagement — it's typically a two-week engagement that surfaces the risks and gives you a concrete roadmap.

Have a system that's holding the team back?

Tell us about it. We'll send a 30-minute architecture read in return.

Start a project