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.
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
Steps
Tangled Monolith
Walls are weak — extraction is painful and risky
Modular Monolith
Strong inner walls — each module is ready to move out
Microservices
Modules become separate services, extracted one by one
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.
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.
| Phase | What happens | Real-life version |
|---|---|---|
| Transform | Build the new microservice beside the old module. Copy or rewrite the logic. | Build the new flat next to the old house. |
| Coexist | Put 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. |
| Eliminate | Once 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
Steps
Transform
Build the new service in parallel with the old module
Coexist
Router sends a small, growing share of traffic to the new service
Eliminate
New service owns 100% of traffic — delete the old 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
NotificationsorReportingmodule 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.
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.
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 breakerStep 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.
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 item | Modular Monolith | Microservices |
|---|---|---|
| Deployment | One unit, ships together | Many units, ship on their own |
| Scaling | Whole app scales together | Each service scales on its own |
| Debugging | Easy, one process | Harder, spread across services |
| Data consistency | Simple, one transaction | Hard, must coordinate over network |
| Running cost | Low | Often much higher |
| Team independence | Limited | High |
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:
- Boundary is clean. The module has its own data and a clear public contract.
- Data is separated. The module's schema can move into its own database.
- Contracts replace calls. In-process calls become HTTP or messages behind the same interfaces.
- Resilience is added. Network calls have timeouts, retries, and a circuit breaker.
- Router is ready. You can send a small slice of traffic to the new service and flip back instantly.
- You watch and grow. Increase traffic only when dashboards stay healthy.
- 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
- Strangler Fig pattern — Microsoft Learn — the official Azure architecture write-up of the pattern.
- Pattern: Strangler application — microservices.io — Chris Richardson's classic catalog entry.
- Strangler Fig pattern — AWS Prescriptive Guidance — a clear three-phase decomposition guide.
- MediatR and MassTransit Going Commercial — Milan Jovanović — what the 2026 licensing changes mean for your projects.
- Wolverine for .NET Microservices — Medium — a look at an open-source messaging alternative.
Related Posts
What Is a Modular Monolith? A Beginner-Friendly Guide for .NET
Understand the modular monolith in simple words: one app, strong internal walls. Learn how it compares to monoliths and microservices, why it is the 2026 default for most .NET teams, and how to build one.
Vertical Slice Architecture in .NET: The Easy Guide
Learn Vertical Slice Architecture in .NET in simple words. Organise code by feature instead of by layer, with diagrams, real examples, a comparison with Clean Architecture, and when to use each.
Monolith to Microservices: How a Modular Monolith Helps
Learn how a modular monolith makes the move from monolith to microservices safe and easy in .NET, using clean boundaries, the Strangler Fig pattern, and small steps.
Migrating a Modular Monolith to Microservices in .NET
A simple, friendly guide to moving a .NET modular monolith to microservices using the strangler fig pattern, YARP, clear boundaries, and safe steps.
Synchronous vs Asynchronous Communication in Microservices (.NET Guide)
A simple, friendly guide to synchronous vs asynchronous communication in microservices, with .NET examples, diagrams, tables, and clear rules on when to use each.
Understanding Microservices: Core Concepts and Benefits for .NET
A beginner-friendly guide to microservices in .NET: what they are, the core ideas behind them, their real benefits and trade-offs, and when to use them.