Back to Articles
December 30, 2025 12 min read

From Monolith to Microservices: The .NET 8 Migration Playbook

Modernize legacy .NET apps with the Strangler Fig pattern. A technical guide on YARP, .NET 8 Keyed Services, and performance benchmarks.

.NET 8 Microservices Legacy Modernization C# Software Architecture
From Monolith to Microservices: The .NET 8 Migration Playbook

From Monolith to Microservices: The .NET 8 Migration Playbook

You have a legacy ASP.NET MVC application. It works. It makes money. But it's slow to deploy, expensive to host, and recruiting developers who want to work on it is getting harder every year.

You know you need to modernize. .NET 8 is faster, leaner, and cloud-native. But the idea of a "Big Bang" rewrite—stopping all feature development for six months to rebuild from scratch—is a non-starter.

This playbook walks you through a production-grade migration strategy using the Strangler Fig Pattern. You'll learn how to route traffic with YARP, leverage .NET 8's Keyed Services for transitional architectures, and measure the ROI with real performance benchmarks. Most importantly, you'll keep shipping features while you migrate.

The Strategy: The Strangler Fig Pattern

The Strangler Fig Pattern involves creating a new system around the edges of the old one, gradually intercepting calls and routing them to the new implementation until the old system is suffocated and can be decommissioned.

In the .NET ecosystem, the tool of choice for this is YARP (Yet Another Reverse Proxy).

Why YARP?

YARP is a high-performance reverse proxy toolkit built on .NET. It allows you to place a lightweight .NET 8 application in front of your legacy monolith. This proxy becomes the new "front door" for all traffic.

Implementation

Instead of rewriting your entire app, you start by configuring YARP to forward everything to your legacy app by default. Then, as you carve out microservices (e.g., an "Orders" service), you add routes to direct specific traffic to the new destination.

The Facade (Program.cs):

var builder = WebApplication.CreateBuilder(args);

// Register YARP and load routing rules from appsettings.json
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

// This single line handles all routing logic
app.MapReverseProxy();
app.Run();

The Routing Logic (appsettings.json):

{
  "ReverseProxy": {
    "Routes": {
      "orders-route": {
        "ClusterId": "orders-service",
        "Match": {
          "Path": "/api/orders/{**remainder}"
        },
        "Transforms": [ { "PathPattern": "{**remainder}" } ]
      },
      "legacy-route": {
        "ClusterId": "legacy-monolith",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    },
    "Clusters": {
      "orders-service": {
        "Destinations": {
          "destination1": {
            "Address": "http://orders-microservice:8080"
          }
        }
      },
      "legacy-monolith": {
        "Destinations": {
          "destination1": {
            "Address": "http://legacy-app-server:80"
          }
        }
      }
    }
  }
}

A Real-World Scenario

Consider a fintech platform with a monolithic ASP.NET MVC app handling payments, user accounts, and reporting. The payments module needs frequent updates to support new providers, but every deployment risks the entire system.

With YARP in place, you extract the payments logic into a new .NET 8 microservice. Update the YARP config to route /api/payments/* to the new service. The monolith still handles accounts and reporting. Your payment team can now deploy independently, multiple times per day, without touching the monolith. The client never knows the difference.

This is incremental migration at its best.

The Tactics: Leveraging .NET 8 Features

Migrating isn't just about moving code; it's about improving it. .NET 8 introduces features that solve specific architectural headaches common in migration scenarios.

Keyed Services for Dependency Injection

In a transitional architecture, you often have two implementations of the same interface: the "Legacy" way (e.g., direct SQL calls) and the "Modern" way (e.g., via a distributed cache or microservice).

Prior to .NET 8, managing this in DI was messy. Now, Keyed Services allow you to register multiple implementations and retrieve the right one explicitly.

When to use this: You're migrating a user service. The monolith reads from SQL Server, but your new microservice uses Redis for session caching. During the transition, some endpoints still need the old SQL implementation while new ones use Redis. Keyed Services lets both coexist cleanly.

Registration:

builder.Services.AddKeyedSingleton<ICache, MemoryCacheService>("local");
builder.Services.AddKeyedSingleton<ICache, RedisCacheService>("distributed");

Usage:

public class ProductService
{
    private readonly ICache _localCache;
    private readonly ICache _distCache;

    public ProductService(
        [FromKeyedServices("local")] ICache localCache,
        [FromKeyedServices("distributed")] ICache distCache)
    {
        _localCache = localCache;
        _distCache = distCache;
    }
}

Performance: The ROI of Migration

Stakeholders care about ROI. The move to .NET 8 isn't just technical hygiene; it's a massive performance upgrade.

  • Dynamic PGO (Profile-Guided Optimization): Enabled by default in .NET 8, this allows the JIT compiler to optimize your code based on real-time usage, often yielding up to 15% throughput improvement without changing a line of code. This matters most for high-traffic APIs where every millisecond of latency costs money.
  • Vectorization (AVX-512): If your application processes large datasets, encryption, or media, .NET 8's support for AVX-512 instructions can process 512 bits of data in a single CPU cycle. Financial services firms running trade reconciliation or risk calculations see dramatic speedups here.
  • Throughput: In TechEmpower benchmarks, ASP.NET Core consistently processes millions of requests per second, dwarfing the capacity of legacy ASP.NET MVC. If you're scaling horizontally to handle load, .NET 8 means fewer servers and lower cloud costs.

The Migration Playbook: Step-by-Step

Phase 1: Assessment & Foundation (Week 1-2)

Risk Level: Low

  1. Identify Bounded Contexts: Don't look at classes; look at domains. Where are the seams? "Billing", "Inventory", "User Management". Use tools like dependency graphs or talk to your senior engineers who know where the coupling is worst.
  2. The Facade: Deploy the YARP proxy. At this stage, it does nothing but forward traffic, but it establishes the infrastructure for the switch. Test it in staging to ensure no performance degradation.

Deliverable: A documented list of migration candidates and a working YARP proxy in production (transparent passthrough mode).

Phase 2: Vertical Slice Extraction (Week 3-6)

Risk Level: Medium

  1. Pick a Pilot: Choose a low-risk module (e.g., "Audit Logs" or "Notifications"). Avoid core business logic for your first extraction.
  2. Build the Service: Create a new .NET 8 Web API. Use System.Text.Json for serialization (it's significantly faster than Newtonsoft). Set up structured logging from day one.
  3. Database Strategy: Initially, let the new service talk to the existing database. Do not try to migrate data and code simultaneously unless necessary. Use a separate schema or read-only views to establish boundaries.

Deliverable: A production-ready microservice deployed alongside the monolith, but not yet receiving traffic.

Phase 3: Cutover & Iterate (Week 7-8)

Risk Level: High (plan for rollback)

  1. Flip the Switch: Update the YARP config to route the specific path to your new service. Do this behind a feature flag or with a phased rollout (e.g., 10% of traffic, then 50%, then 100%).
  2. Monitor: Watch for latency or 500 errors. YARP's logging is your best friend here. Set up alerts for error rate spikes and latency degradation.
  3. Delete Legacy Code: This is crucial. Once the new service is stable (1-2 weeks of production traffic with no incidents), delete the old code from the monolith. If you don't, it will be used again by accident.

Deliverable: First microservice live in production, monolith code deleted, lessons documented.

Phase 4: Optimization (Ongoing)

Risk Level: Low

  1. Native AOT: For small, stateless microservices, enable Native AOT. This compiles your app to native code, resulting in near-instant startup times and a tiny memory footprint—perfect for serverless environments like AWS Lambda or Azure Container Apps.
  2. Observability: Add OpenTelemetry tracing to visualize request flows across the monolith and microservices. This is critical as you extract more services.

Deliverable: Optimized services with measurable performance improvements and production telemetry.

By The Numbers

  • 3x Average throughput increase moving from .NET Framework 4.8 to .NET 8.
  • 50% Reduction in memory footprint for containerized workloads.
  • <100MB Size of a typical Alpine Linux container image for .NET 8, compared to gigabytes for Windows Server Core.
  • 15+ Core tech stacks mastered by our internal vetting team, ensuring we understand the nuances of your migration.

Frequently Asked Questions

Can I mix .NET Framework and .NET 8?

Yes, but not in the same process. They must run as separate applications. YARP bridges this gap by making them appear as a single API to the client.

How do I handle authentication across the monolith and microservices?

This is the hardest part. A common strategy is to have the YARP proxy handle authentication (e.g., validating a JWT) and pass user claims downstream via headers. Alternatively, share the Data Protection keys between the apps if they are both on compatible .NET versions (though this is tricky with Framework 4.8).

Should I use a shared database?

Ideally, no. Microservices should own their data. However, during migration, a "Shared Database" pattern is a pragmatic intermediate step. Just ensure you have a plan to eventually decompose the database schema.

How long does a full migration take?

It depends on the size of the monolith. For a medium-sized app (50-100K LOC), expect 6-12 months to extract the core domains into microservices. The key is that you're shipping features the entire time. The first extraction (Phases 1-3) takes 6-8 weeks. After that, each additional service takes 3-4 weeks as your team learns the pattern.


Ready to modernize your engineering team along with your tech stack? OneCubeStaffing connects you with senior engineers who have executed these exact migrations. Contact us to build your specialized migration team.

Looking for Your Next Role?

Let us help you find the perfect software engineering opportunity.

Explore Opportunities