Skip to main content
SEMastery
Architectureintermediate

Breaking It Down: How to Migrate Your Modular Monolith to Microservices

A friendly, step-by-step guide to safely move from a .NET modular monolith to microservices using the Strangler Fig pattern, without a risky big-bang rewrite.

16 min readUpdated April 16, 2026

Moving out of a shared house, one room at a time

Think about a big joint family living in one large house. Everyone shares the same kitchen, the same water tank, and the same front door. For years this works well. The family is close, costs are low, and everyone knows what is going on.

Then the family grows. One son gets married and wants his own kitchen. One daughter starts a tailoring business and needs a separate room with its own entrance for customers. The shared house starts to feel tight.

Now, you have two choices.

The risky choice is to knock down the whole house and build a brand-new colony of separate flats all at once. While you build, nobody has a place to live. If anything goes wrong, the whole family is on the street. This is the dreaded big-bang rewrite, and it fails far more often than people admit.

The smart choice is to move out one family member at a time. First, build one small separate flat for the newlyweds. They move in. Everyone tests that it works, that the water reaches it, that the road connects. Only then do you build the next flat. The old house keeps running the whole time. Nobody is ever homeless.

That second choice is exactly how you migrate from a modular monolith to microservices. You do it slowly, one module at a time, with the old system always running. The pattern that makes this safe has a lovely name: the Strangler Fig.

Why a modular monolith makes this easy

Before we move out, let us be thankful for good walls.

A modular monolith is one application that you deploy as a single unit, but inside it is split into clear modules like Orders, Billing, and Shipping. Each module owns its own logic and its own data. Modules talk to each other only through public contracts, never by reaching into each other's internal tables.

Those strong inner walls are a gift when you migrate. A module with a clean boundary is like a family member who already packs their own bags, keeps their own things, and does not borrow other people's stuff. Moving them out is simple.

A tangled monolith, on the other hand, is like a person whose belongings are scattered in every room. You cannot move them without touching the whole house. That is why we always say: build a clean modular monolith first, then extract.

The Journey, Not a Jump

Tangled Monolith
Modular Monolith
Microservices

Steps

1

Tangled Monolith

Walls are weak — extraction is painful and risky

2

Modular Monolith

Strong inner walls — each module is ready to move out

3

Microservices

Modules become separate services, extracted one by one

You do not leap straight from a monolith to microservices. The modular monolith is the safe stepping stone in the middle.

The Strangler Fig: replace slowly, never all at once

The Strangler Fig pattern is named after a real tree. The strangler fig grows around an existing tree, slowly wrapping it. Over years, the fig becomes strong enough to stand on its own, and the old tree inside fades away. The new structure grows around the old one until the old one is no longer needed.

Software migration works the same way. You do not delete the monolith first. You grow new services around it, send them more and more work, and only remove the old code once the new service has fully taken over.

The trick that makes this possible is a router (often called a proxy or an API gateway) placed in front of your app. Clients never call the monolith directly. They call the router. The router decides where each request goes.

Figure 1: A router sits in front. At first, everything goes to the monolith. Over time, the router sends more traffic to new services.

At the start, the router sends almost everything to the monolith. As each new microservice becomes ready, you flip a switch and the router sends that slice of traffic to the new service instead. The client never notices. It always just calls the router.

The three phases of a Strangler Fig migration

People who write about this pattern usually describe three phases. They are easy to remember.

PhaseWhat happensReal-life version
TransformBuild the new microservice beside the old module. Copy or rewrite the logic.Build the new flat next to the old house.
CoexistPut a router in front. Send a small slice of traffic to the new service. Watch closely.Let the newlyweds move in, but keep the old room ready just in case.
EliminateOnce the new service handles everything well, route all traffic to it and delete the old module.Once the flat works fully, clear out and lock the old room.

The most important phase is coexist. This is where both the old and new versions run at the same time. You start small, maybe sending only 5% of traffic to the new service. You watch your dashboards. If something breaks, you flip the router back to the monolith in seconds. There is no panic and no downtime.

The Three Phases for One Module

Transform
Coexist
Eliminate

Steps

1

Transform

Build the new service in parallel with the old module

2

Coexist

Router sends a small, growing share of traffic to the new service

3

Eliminate

New service owns 100% of traffic — delete the old module

Each module you extract walks through these same three phases. Repeat for the next module.

Step 1: Choose the right module to extract first

Do not start with your hardest, most central module. That is like trying to move the family grandmother, who knows everyone and touches everything, on day one. Start with someone who packs light.

A good first module to extract usually has these signs:

  • It is loosely coupled. It does not constantly reach into many other modules.
  • It has a clear boundary, with its own data and its own public contract.
  • It gives a real benefit when split out, such as needing to scale on its own. A common example is a Notifications or Reporting module that has very different load from the rest of the app.

The cleaner the boundary, the smaller and safer the extraction. This is the payoff for all the discipline you put into building a proper modular monolith.

Figure 2: A decision flow for picking your first module to extract.

Step 2: Give the module its own data

Inside a modular monolith, modules often share one physical database but keep separate schemas, like orders.Orders and billing.Invoices. This is fine while everything runs together.

A microservice, though, must own its data fully. Two services should never share one database table. If they did, a change in one service could secretly break the other, and you would lose the independence that microservices are supposed to give you.

So part of extraction is giving the module its own database. Because a well-built module already kept its data in its own schema, and nobody else was ever allowed to touch it, lifting it out is clean. You move that schema into a separate database, and the new service talks only to it.

Figure 3: Before, modules share one database with separate schemas. After, the extracted service gets its own database.

Step 3: Replace in-process calls with network calls

Inside the monolith, when Shipping needed an order's address, it called a simple public interface and got an answer instantly. There was no network, no waiting, and no chance of a dropped call.

// Inside the monolith — fast, in-process, always works
public interface IOrdersApi
{
    Task<AddressDto?> GetShippingAddress(Guid orderId);
}

Once Orders becomes its own service, that same call now crosses the network. The network can be slow. It can drop. The other service can be busy or restarting. So your code must become a little more careful. You still depend on the same kind of interface, but the implementation now makes an HTTP call and handles failure.

// After extraction — same shape, but now it calls another service over HTTP
public sealed class OrdersApiClient : IOrdersApi
{
    private readonly HttpClient _http;
 
    public OrdersApiClient(HttpClient http) => _http = http;
 
    public async Task<AddressDto?> GetShippingAddress(Guid orderId)
    {
        // This call can fail or time out — the network is not free
        var response = await _http.GetAsync($"/orders/{orderId}/address");
        if (!response.IsSuccessStatusCode)
            return null;
 
        return await response.Content.ReadFromJsonAsync<AddressDto>();
    }
}

Notice the beautiful part: the interface did not change. Code that used IOrdersApi still works exactly the same. You only swapped the hidden implementation. This is the reward for programming against contracts inside your modular monolith. Your callers never had to know whether Orders lived next door or across the network.

To survive a flaky network, you wrap these calls with resilience. In modern .NET you can add timeouts, retries, and a circuit breaker with very little code using the standard resilience handler.

// Add resilience so one slow service does not freeze everything
builder.Services
    .AddHttpClient<IOrdersApi, OrdersApiClient>(c =>
        c.BaseAddress = new Uri("https://orders-service"))
    .AddStandardResilienceHandler(); // retries, timeouts, circuit breaker

Step 4: Talk through messages, not just direct calls

Direct HTTP calls are fine when one service needs an answer right now. But a big trap in microservices is making every service call every other service directly. Soon you have a tangled web of network calls. If one service is slow, the slowness spreads everywhere. People call this a distributed big ball of mud, which is even worse than a normal one.

The cleaner way for many cases is messaging. Instead of Orders calling Billing and Shipping directly, Orders simply announces an event: "an order was placed." It does not know or care who listens. Billing and Shipping each subscribe and react in their own time through a message broker like RabbitMQ or Azure Service Bus.

// Orders just announces the news — it does not call anyone directly
public sealed record OrderPlaced(Guid OrderId, decimal Total);
 
// Later, Orders publishes the event to a broker
await _publisher.Publish(new OrderPlaced(order.Id, order.Total));

The wonderful thing is that this is the same shape you already used inside the modular monolith. There, you published OrderPlaced in-process. Now you publish it to a real broker over the network. The publishing code barely changes. This is why a modular monolith built with in-process events is such a smooth launchpad.

Figure 4: With messaging, Orders announces an event once. Billing and Shipping react on their own, with no direct calls.
⚠️

A quick note on tools for 2026: the popular libraries MassTransit and MediatR have moved to a commercial (paid) license. MassTransit v8 stays free, but newer versions need a paid plan for many companies. Open-source, MIT-licensed alternatives include Wolverine, Paramore Brighter, and Rebus, or you can use the raw broker clients like RabbitMQ.Client and Azure.Messaging.ServiceBus. Pick based on your budget and needs, and check the current license before you commit.

Step 5: Route a small slice of traffic, then grow it

Now the new service exists and has its own data. It is time for the coexist phase. You point the router at the new service for just a tiny slice of traffic.

Start with read-only traffic if you can, because reads are safer than writes. Send maybe 5% of read requests to the new service while 95% still go to the monolith. Watch your logs and dashboards. Are the answers correct? Is it fast enough? Are errors low?

If everything looks healthy, increase the slice: 25%, then 50%, then 100% of reads. Once reads are stable, do the same careful dance for writes. If anything goes wrong at any step, you flip the router back to the monolith instantly. This safety valve is the whole reason the pattern is so low-risk.

Compared itemModular MonolithMicroservices
DeploymentOne unit, ships togetherMany units, ship on their own
ScalingWhole app scales togetherEach service scales on its own
DebuggingEasy, one processHarder, spread across services
Data consistencySimple, one transactionHard, must coordinate over network
Running costLowOften much higher
Team independenceLimitedHigh

This table is a reminder of what you gain and what you pay. You move to microservices to gain independent scaling and independent deployment. You pay for it with harder debugging, harder data consistency, and higher cost. Only cross this bridge when the gains are worth the price for a specific module.

Step 6: Eliminate the old module

Once the new service handles 100% of its traffic and has been stable for a good while, you finish the job. You delete the old module's code from the monolith and remove its now-unused tables. The router no longer mentions the monolith for that feature.

The old room is now empty and locked. The family member has fully moved into their new flat. Then you take a breath, pick the next module, and repeat the whole gentle process.

💡

Do not rush to delete the old code. Keep it in place, but unused, for a little while after you reach 100% traffic. If a hidden problem appears, you still have an instant way back. Delete only when you are confident. There is no prize for deleting code one day sooner.

Common mistakes to avoid

Migrations go wrong in a few predictable ways. Watch for these:

  • The big-bang rewrite. Trying to convert everything at once is the classic trap. It takes far longer than planned, and during that time you cannot ship anything useful. Always extract one module at a time.
  • Extracting a tangled module. If a module still reaches into other modules' internals, fix the boundary inside the monolith first. Never try to pull out a module whose bags are scattered across the house.
  • Sharing a database between services. If two services read and write the same table, they are not really separate. You have built a "distributed monolith," which has all the cost of microservices and none of the independence.
  • A chatty web of direct calls. If every service calls many others synchronously, one slow service drags down the rest. Prefer messaging and events where you can.
  • No way back. If you cannot instantly route traffic back to the monolith, you have removed your safety net. Always keep the router switch ready.
🚨

The single most dangerous decision is to migrate when you do not actually need to. Microservices cost real money and real complexity. If your modular monolith is serving you well, the wisest move is often to stay. Migrate one module only when a clear, specific pain forces your hand.

A simple checklist for each module

When you are ready to extract a module, walk through this short list:

  1. Boundary is clean. The module has its own data and a clear public contract.
  2. Data is separated. The module's schema can move into its own database.
  3. Contracts replace calls. In-process calls become HTTP or messages behind the same interfaces.
  4. Resilience is added. Network calls have timeouts, retries, and a circuit breaker.
  5. Router is ready. You can send a small slice of traffic to the new service and flip back instantly.
  6. You watch and grow. Increase traffic only when dashboards stay healthy.
  7. You eliminate last. Delete the old code only after the new service is fully stable.

Follow this list for one module, then start again for the next. Slow and steady wins this race every time.

Quick recap

  • Migrate one module at a time, never with a risky big-bang rewrite.
  • The Strangler Fig pattern grows new services around the old monolith using a router in front, so the app never goes down.
  • The three phases are Transform (build beside the old), Coexist (route a small, growing slice of traffic), and Eliminate (delete the old code last).
  • A clean modular monolith is the perfect launchpad: clean boundaries make each extraction small and safe.
  • Each extracted service needs its own database; never share tables between services.
  • In-process calls become HTTP or messages behind the same interfaces, wrapped in resilience for the network.
  • In 2026, check licensing: MassTransit and MediatR are now commercial, while Wolverine, Brighter, and Rebus are open-source choices.
  • Migrate only when a real, specific pain demands it. If your modular monolith works well, staying is often the smartest move.

Moving out of a shared house is a big step, but done one room at a time, it is calm and safe. Keep your walls clean, grow the new around the old, and only ever move out the family members who are truly ready. That is how you break it down without breaking it.

References and further reading

Related Posts