← All articles
Technology · September 1, 2026 · 6 min read

From Monolith to Microservices in .NET: A Practical, Low-Risk Migration Playbook

A step-by-step playbook for migrating a .NET monolith to microservices using the strangler fig pattern, YARP, and clear service boundaries.

"We need to move to microservices" is one of the most common sentences I hear when I'm brought in to look at an ageing .NET system. Sometimes it's the right call. Often, the real problem is something else: slow deployments, a tangled data model, or a team that can't ship without stepping on each other's toes.

In this post I'll share the approach I use when a migration genuinely makes sense: how to decide, how to find boundaries, and how to move incrementally without a risky "big bang" rewrite.

First, Be Honest About Why You're Migrating

Microservices are an organisational and operational trade-off, not a free upgrade. Before writing a line of code, I ask teams to write down the specific pain they want to remove. Good reasons usually look like this:

  • Independent deployment: one part of the system changes weekly while the rest is stable, and every release drags the whole application along.
  • Independent scaling: a single module (search, reporting, file processing) needs far more resources than everything else.
  • Team autonomy: several teams work in one codebase and constantly block each other.
  • Technology isolation: a legacy area on an old framework is holding back the rest of the system.

Weak reasons include "it's modern," "Netflix does it," or "the code is messy." Messy code in a monolith becomes messy code spread across a network, with latency and partial failures added on top. If the main problem is code quality, a modular monolith is frequently the better first step, and it happens to be excellent preparation for microservices later.

Step 1: Find the Real Boundaries

The most expensive mistake in a migration is splitting along the wrong lines. Services cut by technical layer ("the database service," "the validation service") create chatty, tightly coupled systems. Services should follow business capabilities: Ordering, Billing, Catalogue, Notifications.

A few techniques I rely on:

  • Event storming workshops with developers and domain experts to map business events ("Order Placed," "Payment Captured") and see where responsibilities cluster.
  • Database analysis: look at which tables are read and written together. Tables that are always joined together usually belong to the same service.
  • Change history: your source control knows which files change together. Frequent co-changes across a proposed boundary are a warning sign.

Step 2: Modularise Inside the Monolith First

Before extracting anything, I make the boundaries explicit inside the existing solution. Each module gets its own project, its own public contract, and ideally its own database schema.

// Ordering module exposes a contract, not its internals
public interface IOrderingModule
{
    Task<OrderSummary> GetOrderAsync(Guid orderId, CancellationToken ct);
    Task<Guid> PlaceOrderAsync(PlaceOrderCommand command, CancellationToken ct);
}

Other modules depend only on this interface, never on the Ordering module's DbContext or entities. Once direct references are gone, extracting the module into its own service becomes a deployment exercise rather than a rewrite. Architecture tests (for example with NetArchTest) can enforce these rules in CI so the boundaries don't quietly erode.

Step 3: Apply the Strangler Fig Pattern

The strangler fig pattern lets you replace the monolith piece by piece. You put a routing layer in front of the application, then gradually redirect specific routes to new services while everything else keeps flowing to the monolith.

In the .NET world, YARP (Yet Another Reverse Proxy) is a great fit because it's just an ASP.NET Core application you can configure and extend:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();
app.MapReverseProxy();
app.Run();
"ReverseProxy": {
  "Routes": {
    "orders": { "ClusterId": "ordering", "Match": { "Path": "/api/orders/{**rest}" } },
    "legacy": { "ClusterId": "monolith", "Match": { "Path": "{**catch-all}" } }
  },
  "Clusters": {
    "ordering": { "Destinations": { "d1": { "Address": "https://ordering-service/" } } },
    "monolith": { "Destinations": { "d1": { "Address": "https://legacy-app/" } } }
  }
}

Clients never notice the change. If the new service misbehaves, you switch the route back. That reversibility is what makes the approach low-risk.

Step 4: Untangle the Data

Code is the easy part; data is where migrations succeed or fail. The goal is that each service owns its data and no other service reads its tables directly. Getting there usually happens in stages:

  1. Separate schemas in the same database, with access only through the owning module.
  2. Synchronise data the new service needs using events, rather than shared tables.
  3. Move the schema into its own database once nothing else touches it.

For reliable event publishing, I use the transactional outbox pattern: write the business change and the outgoing event in the same database transaction, then publish the event from a background worker.

await using var tx = await db.Database.BeginTransactionAsync(ct);

db.Orders.Add(order);
db.OutboxMessages.Add(OutboxMessage.From(new OrderPlaced(order.Id, order.Total)));

await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
// A hosted service later publishes pending outbox messages to the broker

This avoids the classic bug where the database commit succeeds but the message is lost (or the reverse). Libraries such as MassTransit include outbox support out of the box, so you rarely need to build it from scratch.

Step 5: Invest in the Platform Before You Need It

Each new service multiplies your operational surface. Before extracting the second or third service, make sure you have:

  • CI/CD per service, with automated tests and repeatable deployments.
  • Containerisation and a consistent hosting model (Kubernetes, Azure Container Apps, AWS ECS, or similar).
  • Observability: structured logging, distributed tracing, and metrics. OpenTelemetry is now well supported in .NET and works with most backends.
  • Resilience: timeouts, retries, and circuit breakers on every network call. Microsoft.Extensions.Http.Resilience makes this straightforward:
builder.Services.AddHttpClient<CatalogueClient>(c =>
        c.BaseAddress = new Uri("https://catalogue-service/"))
    .AddStandardResilienceHandler();

Without these foundations, debugging a request that crosses four services becomes guesswork.

Step 6: Extract, Measure, Repeat

Start with a service that is valuable but not mission-critical: well-understood, loosely coupled, and with clear ownership. Notifications or reporting are often good candidates. Extract it, run it in production, and learn from it. Measure what matters: deployment frequency, lead time for changes, error rates, and latency. If the first extraction didn't improve anything, stop and understand why before continuing.

Common Pitfalls to Avoid

  • The distributed monolith: services that must be deployed together or call each other synchronously in long chains. You get all the costs and none of the benefits.
  • Shared databases: they keep services coupled at the deepest level.
  • Too many, too small: start with fewer, coarser services. Splitting later is easier than merging.
  • Ignoring the team structure: services work best when each one has a clear owning team.

Final Thoughts

A successful migration from a .NET monolith to microservices is rarely a dramatic rewrite. It's a disciplined sequence of small, reversible steps: clarify the goal, modularise, route traffic gradually, untangle the data, and build the platform as you go. Done this way, the business keeps shipping features throughout the migration. In my experience, that's the only kind of migration that really succeeds.

If you're weighing up a migration and want a second opinion on boundaries or sequencing, feel free to reach out.

.NETMicroservicesASP.NET CoreSoftware ArchitectureMigration
MF
About Muhammad Faisal

Senior .NET Full Stack Developer, Software Architect and Consultant with 10+ years in tech. Writes about software development, cloud architecture and building high-performing teams.

See my work on faisaldeveloper.com ↗

Related articles