A monolith is not a failure. Most successful products start as one, because a single codebase with a single database is the fastest way to learn what customers want. The trouble starts later: releases slow down, a change in billing breaks search, and nobody wants to touch the order module. At that point someone proposes a migration, and the risk is that the cure does more damage than the illness.
This post covers how to decide whether to migrate, how to find where to cut, and how to move a live system piece by piece without stopping the business that depends on it.
Decide whether to migrate at all
Start with the problem, not the architecture. "We need microservices" is a solution. The problem is usually one of these:
- Releases are slow or risky because every change touches everything.
- One part of the system needs to scale very differently from the rest.
- Teams block each other because they all work in the same code.
- A piece of the stack is out of support and cannot be upgraded in place.
Write the problem down and ask what the cheapest fix is. Slow releases are often a build pipeline and test problem, not an architecture problem. One hot endpoint can often be scaled with a cache, a queue or a read replica. An outdated framework can sometimes be upgraded module by module inside the same codebase.
If the cheap fixes have been tried, or clearly will not reach far enough, a migration is justified. Even then, it should have a stated goal that a non-engineer can check, such as "we can release the checkout without a full regression run", rather than "we are on the new architecture".
Find the seams before you cut
A seam is a place where the system can be split with the least disturbance. Good seams follow the business, not the folder structure. Look for areas that:
- Have their own vocabulary. If the warehouse team says "shipment" and the sales team says "order" for related but different things, that difference is a boundary.
- Change for their own reasons. Code that is always edited together belongs together. Code that changes on a different rhythm is a candidate to move.
- Talk to the rest of the system through a few clear calls rather than dozens of shared tables.
Version history helps here. Files that change together in the same commits reveal the real coupling, which is often different from the intended design. Draw the dependencies you find, including the database ones, before choosing the first piece.
The first piece to move should be valuable enough to matter but small enough to finish. Notifications, reporting, search and file handling are common first candidates because they sit at the edge. The core domain, the part that makes the product what it is, usually comes later, once the team has practised on something less critical.
Strangle the old system one route at a time
The strangler fig pattern, named by Martin Fowler after a plant that grows around a host tree, is the safest general approach. You do not rewrite the monolith. You put something in front of it and move traffic away from it one piece at a time.
In practice it runs like this:
- Put a routing layer in front of the monolith. This can be a reverse proxy, an API gateway, or a thin facade in the application itself.
- Build the new version of one capability behind that layer.
- Send a small share of traffic, or one group of users, to the new version, and compare results with the old one.
- Widen the share until the old code path receives nothing, then delete it.
The last step matters most. A migration that adds new code without deleting old code leaves you with two systems to run. Every slice should end with the old path removed and the routing rule simplified.
Where the new and old versions must behave the same, run them side by side for a while. Send each request to both, return the old answer to the user, and log any difference. This shadow period finds the undocumented behaviour that every old system carries.
Settle who owns the data
Code is the easy part. The shared database is where most migrations stall. If the new component and the monolith both read and write the same tables, you have not separated anything; you have added a second writer to a schema that nobody can now change.
The rule to aim for is that each piece of data has one owner. Only the owner writes it. Everyone else asks the owner, through an API or by listening to events the owner publishes.
Getting there is gradual:
- Map who writes what. List every table and every piece of code that writes to it. Tables written from many places are the ones to worry about.
- Route writes through one place first. Before moving data, make the monolith itself write each table through a single module. This alone often removes half the coupling.
- Copy, then switch. Give the new component its own store, keep it in step with the old one (change data capture or dual writes with reconciliation), and switch reads over before writes.
- Check the copies agree. Run a reconciliation job that compares old and new and reports differences. Do not cut over until it has been quiet for a while.
Reports and analytics often read from everywhere. Give them their own read model, fed by events or replication, so they stop being a reason to keep the old schema alive.