Strangler Fig Pattern for Legacy .NET Apps

8 min read
Share:

Introduction

Someone in leadership says “we need to move to .NET 8” or “let’s get this thing into Azure.” Cool. But when you ask what that actually means — which apps, which order, what happens to production while you’re mid-migration — you often get a vague answer and a timeline that makes no sense.

I’ve been in that room. The legacy app still pays the bills. You can’t freeze feature work for eighteen months while a rewrite catches up. And you definitely can’t risk a weekend cutover on a system nobody fully understands anymore.

What usually helps — not always, but often — is the Strangler Fig pattern. You keep the old .NET app running. You peel off one piece at a time. New stuff gets built beside it, not instead of it. Traffic moves over when you’re ready, not when a project plan says you should.

Here’s how I’ve seen .NET teams make that work — where to cut first, how YARP fits in, and what usually goes wrong.

Body

The rewrite that never ships

I’ve seen this play out more than once.

A team picks up a .NET Framework app — MVC, Web API, sometimes with a WCF layer everyone’s afraid to touch. Leadership wants .NET 8. Or cloud. Or both. Someone in a meeting says “let’s just rewrite it.” Everyone nods. Six months later the new codebase is maybe 40% done, production is still on the old system, and nobody brings up “migration” in meetings anymore.

That’s not really a coding problem. It’s a planning problem. And honestly, Strangler Fig exists because full rewrites keep failing in exactly this way.

What the Strangler Fig pattern means

Okay, the name is weird. A strangler fig is a plant that grows around a tree and eventually takes its place. Martin Fowler used that image years ago to describe how you’d modernize software — not by swapping everything out on a deadline, but by slowly building around what’s already there.

For .NET teams, the practical version is simpler: put YARP or a gateway in front, pull features into new ASP.NET Core services one at a time, and leave the monolith running until each slice is proven. No Saturday-night switch — just small moves you can roll back.

Why this works well for legacy .NET

Most old .NET Framework apps I’ve worked on look familiar: giant solution file, one SQL Server everyone shares, auth tangled into the web project, a Windows Service doing something important at 2 AM, and a DLL or two that hasn’t been touched since 2016.

Trying to untangle all of that in one project is brutal. Strangler Fig gives you permission to fix one headache first — reporting, notifications, a customer API — while the rest of the app keeps chugging along. For a lot of teams, that’s the only approach that doesn’t get blocked by risk committees.

Step 1: Find a seam, not a layer

The mistake I see most often? Teams try to extract the “data layer” or “business layer” first because it looks clean on a whiteboard. In a codebase that’s 10+ years old, those layers are usually a mess — session state, stored procedures, business rules all mixed together.

Look for a business feature with a small surface area instead:

  • A read-heavy API your mobile app calls
  • Reporting — lots of reads, not much writing
  • User profiles or document uploads
  • A brand-new feature — just build it outside the monolith from day one

I’d stay away from core order processing, anything with distributed locks, or shared login/session stuff unless you’ve thought through identity properly. One question helps: if this breaks for a day, how bad is it? Start where the answer is “annoying, not catastrophic.”

Step 2: Put a router in front

First thing we usually do once we’ve picked a slice: stop exposing legacy and new stuff on separate URLs. One entry point. One place to decide where traffic goes.

If the team is mostly .NET, we reach for YARP a lot. Bigger setups might need a full API gateway; a BFF helps when clients need responses stitched from multiple backends.

Here’s a simple YARP setup we use fairly often — anything under /api/reports/* goes to the new service, everything else falls back to legacy:

{
  "ReverseProxy": {
    "Routes": {
      "reports-route": {
        "ClusterId": "reports-cluster",
        "Match": { "Path": "/api/reports/{**catch-all}" }
      },
      "legacy-fallback": {
        "ClusterId": "legacy-cluster",
        "Match": { "Path": "{**catch-all}" }
      }
    },
    "Clusters": {
      "reports-cluster": {
        "Destinations": { "reports-api": { "Address": "https://reports.internal/" } }
      },
      "legacy-cluster": {
        "Destinations": { "legacy-app": { "Address": "https://legacy.internal/" } }
      }
    }
  }
}

Host side is short — register reverse proxy, load config, map it:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();
app.MapReverseProxy();
app.Run();

What I like about this stage: once routing works, shifting a path from old to new is often just a config edit. You’re not redeploying the whole monolith to test one change.

Diagram showing a legacy .NET monolith still running while YARP routes some traffic to new ASP.NET Core services like Reports API and Profile API

Pretty much how it works in real projects — legacy app stays up, YARP sits in the middle, and new services take over one route at a time.

Step 3: Build the new slice

Keep the first service boring. ASP.NET Core Web API, simple DTOs, logging, health checks. Don’t over-engineer it on day one.

It’s fine if the new service still reads from the same SQL Server at first. Not ideal long term, but realistic. We’ve done that plenty of times.

app.MapGet("/api/reports/summary", async (
    IReportService reports, CancellationToken ct) =>
{
    var summary = await reports.GetSummaryAsync(ct);
    return Results.Ok(summary);
});

Version one isn’t about perfect architecture. It’s about proving you can send real traffic to new code and sleep through the night.

Step 4: Move traffic, then actually delete the old code

Big-bang cutovers are where things fall apart. Start with internal users. Then maybe 5% of production traffic. Then more. And know how to roll back before you flip anything — changing one route back to legacy should take minutes, not a war room.

Also — and teams skip this all the time — delete the legacy path once the new one is stable. I’ve seen projects run two systems for a year because nobody wanted to remove the old code. That’s not modernization. That’s just double the maintenance.

Before you call a slice “done,” I’d look for something like:

  • New service has handled all traffic for that feature for at least 30 days
  • Error rates look normal
  • No open critical bugs tied to the migration
  • Legacy code for that route is actually removed

Things that will bite you

Shared database. This one catches people off guard. You stand up a shiny new microservice, but both apps still write to the same tables. On paper you’ve “extracted” something. In reality you’ve got the same coupling with extra deployment steps.

Session and login. If the legacy app uses Forms Authentication or in-proc session, don’t assume your new API can just plug in. We had a project stall for weeks because nobody mapped out how logged-in users would hit the new routes. Sometimes identity has to be the first slice, not the third.

Too many services, too fast. Five microservices sharing one database, deploying in a chain — often slower than the monolith you started with. One at a time is boring. Boring is fine.

Logging. Once traffic splits, “where did this request go?” is a real question. Set up correlation IDs and OpenTelemetry before production routing, not after a late-night incident.

What the first 90 days might look like

Timelines vary — routing alone took three weeks on one project, eight on another. But when someone asks what the first three months look like, this is roughly what I say.

Weeks 1–3: Map what the monolith actually does. Pick one seam that won’t blow up if it wobbles. Get YARP or a gateway in front. Add correlation IDs — basic, but easy to skip and regret.

Weeks 4–8: Build the first ASP.NET Core piece. Keep scope tiny. Non-prod first, then shadow traffic or internal users who’ll find the edge cases.

Weeks 9–12: Route real production traffic for that slice. Document rollback while you’re calm. If numbers hold for a few weeks, delete the legacy path — not “cleanup in Q4,” actually delete it.

Conclusion

You don’t need a three-year rewrite that ships half-finished. Strangler Fig is slower on paper and faster in reality, because you keep delivering while you move.

Pick a small slice. Get routing in place. Move traffic when you’ve got a rollback plan. Kill the old code when you’re sure. And when someone pushes for a full rewrite, ask which part of the business they’re fine pausing for six months. That tends to reset the conversation pretty quickly.

Working through a legacy .NET migration? Get in touch or see our Application Modernization services. Heading cloud-native next? Read our .NET Aspire post. Done a strangler migration before — what did you pull out first? Leave a comment.

Leave a Reply

Your email address will not be published. Required fields are marked *