Replace legacy systems one route at a time

ยท 2 min read

Big-bang rewrites of legacy apps tend to run late and launch with surprises. The strangler fig pattern moves functionality to the new system piece by piece, with a working system at every step.

Most teams eventually face an old system that's hard to change: an ASP.NET Web Forms or MVC 5 app on .NET Framework, years of business rules nobody fully remembers, and no tests. The tempting plan is to rewrite it from scratch and switch over in one go.

That plan has a track record. The rewrite takes longer than expected, the old system keeps changing while you build, and the cutover day reveals all the behavior nobody knew about.

The strangler fig

The strangler fig pattern, named by Martin Fowler after a vine that slowly grows around a tree until it replaces it, takes the opposite approach:

  1. Put a routing layer in front of the legacy system. At first it sends everything to the old app.
  2. Build one piece of functionality in the new system.
  3. Route just that piece to the new system.
  4. Repeat until nothing goes to the old system, then switch it off.

At every step you have a working system in production, and each step is small enough to roll back.

The routing layer with YARP

For web apps, a reverse proxy is the natural router, and YARP (Yet Another Reverse Proxy) runs inside an ASP.NET Core app:

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

app.MapControllers();      // routes the new app handles itself
app.MapReverseProxy();     // everything else goes to the legacy app

app.Run();
{
  "ReverseProxy": {
    "Routes": {
      "legacy": { "ClusterId": "legacy", "Match": { "Path": "{**catch-all}" }, "Order": 1000 }
    },
    "Clusters": {
      "legacy": { "Destinations": { "main": { "Address": "https://legacy.internal.example.com/" } } }
    }
  }
}

The new ASP.NET Core app is the front door. Endpoints it implements are handled directly, and the catch-all route with a high Order value proxies everything else to the old app. Moving a feature is a matter of implementing its endpoints. Microsoft uses the same approach in its guidance for incrementally migrating ASP.NET Framework apps to ASP.NET Core.

Pick the order carefully

  • Start with something small and low-risk to prove the routing, deployment and monitoring work.
  • Then go after the areas that change most often. That's where the old system costs you most, and where the new one pays off soonest.
  • Leave stable, rarely touched areas for last. Some may never be worth moving.

The hard part is data

Routing requests is easy. Shared data is hard. While both systems run, they often need the same database. Options include letting the new system read and write the legacy database at first, then moving each area's tables to the new system's database along with its feature. Whatever you choose, decide for each table which system owns it at each stage.

Shared concerns such as login and sessions need a plan too, so users don't notice which system served a page.

Takeaway

Don't rewrite a legacy system in one go. Put a routing layer in front of it, move features across one route at a time, starting small, and plan data ownership as carefully as the code. You deliver value continuously, and every step can be rolled back.