How to Migrate a Monolith to Microservices
Migrate a monolith to microservices incrementally, never in one cutover. Map the domain to find natural service boundaries, stand up a strangler facade in front of the monolith, and extract one slice at a time — building the service, proving it behaves identically to the monolith, and shifting its traffic gradually with rollback ready. Start with a low-risk, high-clarity slice to prove the pattern, sequence the rest by business value, and retire the monolith piece by piece as it empties of responsibility.
Parts 1 through 3 settled the strategy: decompose only under real pressure, do it incrementally, and refactor rather than rewrite. This part is the playbook — how a running monolith actually becomes a set of services without the business ever feeling a cutover.
The whole method rests on one principle established earlier in this series and across our legacy modernization field guide: you do not migrate the monolith. You strangle it. One slice at a time, old and new running side by side, until the monolith has nothing left to do.
First, be sure you should
Before the playbook, the harder question: should you split this monolith at all? For a lot of systems the honest answer is no, and a senior engineer earns their keep by saying so.
A monolith is not a failure state. A well-structured one is a genuinely good architecture, and it is the right architecture for most systems most of the time. One codebase, one deploy, one database, and a call between two modules is a function call rather than a network round trip that can time out. Microservices trade that simplicity away, and they trade it for specific things: independent scaling of parts with very different load, independent deployment so teams stop waiting on each other, and fault isolation so one component’s failure does not take the rest down. Those are real benefits. They are also the only reasons that justify the cost.
So the test is not “is this a monolith” but “is a pressure the monolith cannot relieve.” Three that qualify:
- Scaling mismatch. One part of the system carries load an order of magnitude different from the rest, and scaling the whole monolith to serve it wastes money the split would save.
- Deployment contention. The team has grown to the point where a single codebase and release train serialize everyone, and shipping has slowed because of it, not because of the code itself.
- Fault coupling. A single non-critical component keeps taking the whole system down with it, and isolating it is the only way to protect the critical paths.
If none of those is real, the right move is usually to modularize the monolith, clean the internal boundaries, and stop there. Splitting a monolith that has no scaling, deployment, or fault pressure buys you network latency, distributed transactions, and an operations burden in exchange for benefits you will not use. That is not modernization. It is resume-driven architecture, and it is more common than anyone admits.
What you gain and what you pay
The trade is worth naming plainly before you commit to it.
| Monolith | Microservices | |
|---|---|---|
| Deployment | One unit, one release train | Independent per service |
| Scaling | Whole app scales together | Per-service, matched to load |
| A call between modules | In-process function call | Network call that can fail or lag |
| Data consistency | One database, transactions are easy | Distributed, eventual, harder |
| Fault isolation | One component can down the system | Failures contained to a service |
| Operational surface | One thing to run and monitor | Many services, a platform to run them |
| Team autonomy | Shared codebase, coordinated releases | Teams own and ship services independently |
| Right when | Load, team, and faults are manageable | A named pressure the single unit cannot relieve |
Read the table as a trade, not a scorecard. Microservices win the rows that matter when you have the pressures above, and lose the rows that matter when you do not. The migration is worth doing exactly when your pressures live in the rows microservices win.
Step 1: Map the domain before you touch the code
The first move is not technical. Before extracting anything, map the business capabilities the monolith performs — ordering, billing, inventory, notifications, identity — and the boundaries between them. These domain seams, not the monolith’s accidental internal structure, are where services should be cut.
This matters because the monolith’s internal organization is usually an artifact of how it grew, not of how the business is actually divided. Cut along the code’s existing fault lines and you get services that are still entangled — the distributed monolith waiting to happen. Cut along domain boundaries and the services are genuinely independent. We give this its own part, because it is the highest-leverage and most error-prone step: Domain-Driven Design for Service Boundaries.
Step 2: Stand up the strangler facade
With boundaries identified, put a strangler facade in front of the monolith. The facade is an intermediary — typically realized through an API gateway or routing layer — that receives every incoming request and decides whether it is handled by the monolith or by an extracted service.
On day one, the facade routes everything to the monolith. Nothing has changed for users. But the seam now exists, and it is the thing that makes everything afterward incremental: as each service comes online, the facade begins routing that service’s traffic to it, a controlled percentage at a time, with the monolith still standing by. The facade is what lets old and new coexist safely and lets cutover be gradual instead of a single flip.
That facade is real engineering you take on specifically to buy reversibility. It is a cost a big-bang doesn’t incur — and it is the best money in the program, because it converts an all-or-nothing bet into a series of small, recoverable steps.
Step 3: Choose the first slice deliberately
The first extraction is special. It is not just shipping a service — it is proving the mechanism: the facade, the build pipeline, the parity process, the cutover procedure, the rollback. So choose a first slice that is high in clarity and low in risk:
- a capability with a clean, well-understood boundary;
- limited data entanglement with the rest of the system;
- a modest blast radius if something goes wrong.
Notifications, a document generator, or a self-contained calculation engine often make good first slices. A core transactional path tangled into half the database does not. Save the hard, high-stakes domains for after the team has the loop working smoothly. Get one slice all the way to production and the rest of the program is repetition of a proven pattern.
Step 4: Extract a slice — the loop
Each slice runs the same loop, and the discipline of repeating it is what keeps the migration safe:
- Build the service for the slice’s capability, on the target stack, with its own data (Part 6 covers how the data comes apart — it is the hard part).
- Prove parity. Before the service carries any real traffic, prove it behaves identically to the monolith for the same inputs. This is not optional and not “we tested it” — it is parity validation: functional equivalence, data integrity, and non-functional parity (latency, security) all demonstrated, often by running shadow traffic through both the new service and the monolith and reconciling the outputs.
- Shift traffic gradually. Once parity holds, the facade begins routing real traffic to the new service — a rising percentage, telemetry watched, rollback one config change away. See the zero-downtime cutover playbook for the mechanics.
- Retire the slice from the monolith. When the service owns 100% of the slice’s traffic and has held, the corresponding code and data leave the monolith. The monolith is now smaller. Repeat.
The reversal worth keeping: with this loop, surprises become tickets, not outages. A slice that misbehaves is caught at the parity gate or rolled back in seconds — it never becomes a production incident.
The loop, on one real slice
Take notifications, a common and well-chosen first slice. In the monolith, sending an email or SMS is a set of functions the ordering, billing, and account modules all call inline. The extraction runs like this. You build a Notifications service with its own datastore for templates and delivery state, and give it a clean API: send this, to this recipient, using this template. You do not point live traffic at it yet. Instead you run it in shadow: every time the monolith sends a notification, the same request is mirrored to the new service, which composes the message and compares its output to the monolith’s without actually sending. You reconcile the differences until they are zero, which is where you discover the undocumented rule that suppresses a certain notification for a certain account tier, the kind of thing no one remembered was there. Only once parity holds do you flip the facade so the monolith calls the new service instead of its inline functions, one caller at a time, billing first, then ordering, then accounts. When every caller routes through the service and it has held under real load, the old inline notification code and its tables leave the monolith. The monolith is smaller, and you have proven the whole loop on a slice that could not take down a payment if it failed.
That is the pattern the rest of the program repeats. The first slice is slow because you are building the machinery. Every slice after it is faster because the machinery already exists.
Step 5: Sequence the rest by value, and know when to stop
After the first proving slice, sequence the remaining extractions by business value and pressure, not by what is technically tidy. Pull out the service that relieves the scaling bottleneck, or unblocks the team that is most serialized, or isolates the component that keeps failing — the pressures you named back in Part 2. Each slice should earn its extraction.
And here is the part most playbooks omit: you may not finish, and that can be correct. The goal was never “everything is a microservice.” It was to relieve specific pressures. It is entirely legitimate to extract the four services that mattered and leave the rest as a well-modularized monolith forever. A migration that stops when the pressure is gone is a success, not an abandoned project.
What this costs, honestly
This approach is not free, and a team should budget for its real costs rather than be surprised by them:
- Two systems coexist for the life of the program — more to monitor, two code paths to reason about, a facade to maintain.
- Temporary integration code between the monolith and new services often has to be built and then thrown away. That is waste by design — the price of never stopping the business.
- The data is the hard part. Untangling a shared database is harder than splitting the code, and it is where migrations most often stall. We give it Part 6 for a reason.
None of these outweigh the risk the incremental shape removes for a large, critical system. But a team that adopts it without budgeting for the coexistence overhead will feel ambushed, and we would rather they not be.
Where this leads
The playbook keeps returning to one phrase — cut along the domain boundaries — and quietly assumes you know where they are. You usually don’t, not precisely, and a wrong boundary is the most expensive mistake in the whole migration. Part 5, Domain-Driven Design for Service Boundaries, is the method for finding the right seams before you cut.
Frequently asked questions
- What is the first step in migrating a monolith to microservices?
- Understand the domain before touching the code. Map the business capabilities the monolith performs and the boundaries between them, so the services you extract follow real domain seams rather than the monolith's accidental internal structure. Only then do you stand up a strangler facade and choose a first slice. Cutting before you understand the boundaries is how a monolith becomes a distributed monolith.
- Which service should you extract first?
- A slice that is high in clarity and low in risk — a capability with a well-understood boundary, limited data entanglement, and modest blast radius if something goes wrong. The first extraction is as much about proving the pattern, the facade, and the parity process as it is about the service itself. Save the most entangled, highest-stakes domains for after the team has the mechanism working.
- How long does a monolith-to-microservices migration take?
- It depends on the system's size and entanglement, and it is the wrong question to anchor on. Because the migration is incremental, value ships continuously — a modernized slice every few weeks — rather than arriving at one distant date. The program runs as long as it needs to, but the business is never waiting on a single cutover, and any slice can be paused or reprioritized as needs change.
- Should you migrate a monolith to microservices at all?
- Often not. A well-structured monolith is a perfectly good architecture, and microservices are a cost you take on to solve a specific problem: independent scaling, independent deployment, or team autonomy at a size where one codebase serializes everyone. If none of those pressures is real, splitting the monolith adds network calls, distributed transactions, and operational overhead in exchange for benefits you will not use. The honest first move is to modularize the monolith and only extract the services that relieve a pressure you can actually name.
- What is a distributed monolith?
- The failure mode where you split a monolith into services that still depend on each other so tightly that nothing can be deployed, scaled, or reasoned about independently. You paid the full price of microservices, the network hops, the distributed failures, the operational complexity, and got none of the benefit, because the services are still one system wearing several process boundaries. It usually comes from cutting along the code's accidental structure instead of real domain boundaries. It is the single most common way these migrations go wrong.
- How do you handle the shared database when splitting a monolith?
- Carefully, and last. The shared database is where most of these migrations actually stall, because a single schema that every module reads and writes is far more entangled than the code above it. The discipline is to give each extracted service ownership of its own data and reach the others only through their service interfaces, never by reaching across into another service's tables. Getting there usually means a period of controlled duplication and synchronization while the data comes apart, which is real work you should budget for rather than discover mid-migration.
- Do you need Kubernetes to run microservices?
- No. Microservices are an architectural choice about how the system is decomposed. Kubernetes is one way to run and orchestrate the result, not a requirement of it. Small service estates run fine on simpler platforms, and adopting a heavy orchestration stack you do not yet need is its own form of premature complexity. Let the number and scaling needs of the services justify the platform, rather than adopting the platform first and looking for services to fill it.