# Migrating a monolith without a meltdown

> How to decide whether a monolith needs migrating at all, and how to move it piece by piece while releases keep shipping and the data stays whole.

Ilya Ismatov · CTO · 24 August 2026 · Rescue

Bitmason: https://bitmason.dev/en/blog/migrating-a-monolith/

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:

1. 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.
2. Build the new version of one capability behind that layer.
3. Send a small share of traffic, or one group of users, to the new version, and compare results with the old one.
4. 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.

## Keep releasing and measure what moved

A migration that stops feature work loses the support of the business within months. Keep releasing throughout. Feature flags let migrated code ship dark and switch on when ready. Short-lived branches and small pull requests keep the old and new code from drifting apart.

A few habits keep the team honest:

- Agree a rough split of effort between migration and features, and review it at each planning session.
- New features in an area that is being migrated go into the new code, not the old.
- Every slice has a rollback that has actually been tested.

Measure progress in terms the stated goal cares about. Useful measures include the share of traffic served by new code, the number of tables with a single owner, lead time from merge to production, and how often a release has to be rolled back. The number of services created is not a measure of progress; it can rise while everything gets worse.

## When a modular monolith is the answer

Many teams discover partway through that what they wanted was boundaries, not network calls. A modular monolith keeps one deployable application and one database server, but divides the code into modules with explicit public interfaces and private internals. Each module owns its tables, and other modules may not reach into them.

This gives most of the benefits people want from services: clear ownership, code that can be reasoned about in parts, and the option to extract a module later. It avoids the costs: network failures between components, distributed transactions, a larger operations burden, and tracing a request across many processes.

A modular monolith is usually the better answer when the team is small enough to coordinate in one room, when the parts scale in similar ways, and when the real pain was tangled code rather than load. Enforce the boundaries with tooling (dependency rules checked in CI) rather than goodwill, or they erode within a few releases.

Extract a module into its own service only when you can name the reason: it needs to scale on its own, it needs a different release rhythm, or a separate team needs to own it end to end.

## Where to start

Write down the problem the migration must solve and how you will know it is solved. Map the real coupling, in code and in the database. Pick one edge capability, put a routing layer in front of the monolith, move that capability across, and delete the old path. Then decide on the next slice with what you learned.

A migration done this way never has a big day where everything changes at once. It is a series of small, reversible steps, each of which leaves the system a little easier to change than before.

## Questions about this article

### How long does a monolith migration usually take?

Longer than the first estimate, because the hard parts only show once you start moving data. Plan in slices that each deliver something on their own, so the work can stop at any point and still have been worth doing.

### Should we freeze new features during the migration?

Rarely. A long freeze costs the business more than the migration saves. Keep a steady share of the team on features and route new work in migrated areas to the new code.

### Can we migrate without a full test suite?

Yes, but write characterisation tests around each part before you move it. They record what the old code does today, so you can prove the new code does the same.

### Do we need microservices at the end of a migration?

No. The goal is a system your team can change safely. For many products that is a modular monolith with clear boundaries, not a fleet of services.

