Monolith → Microservices
Migrating a monolith to microservices means moving an existing system to services without stopping delivery. The migration that works is not a project with a target architecture and an end date — it is a sequence of individually justified extractions behind a facade, each small enough to reverse. The hard part is never the code; it is splitting the data.
- Pattern
- Strangler fig
- Unit of work
- One extraction
- Hardest part
- Splitting the data
Start with the constraint, not the target
Most migrations begin with a picture of the destination — a diagram of twelve services — and work backwards. That ordering is why so many of them run for two years and end with eight services, one monolith that still contains the important logic, and a team that is slower than when it started.
The ordering that works starts from a named constraint you can measure today. "Payments and the catalogue are on one release train and payments waits three weeks for an unrelated regression pass." "Image processing needs eight times the memory of everything else and we pay for that across forty instances." "The recommendation engine has to be in Python and the rest is not." Each of those names a specific thing to extract and gives you a way to tell afterwards whether it worked.
The strangler fig pattern
The pattern is named after a plant that grows around a tree, gradually takes over its structural role, and leaves the original hollow. Applied to software: put a facade in front of the existing system, route everything through it, then move functionality behind that facade one route at a time.
The facade goes in first and it goes in before anything is extracted. That order matters: with the facade in place, the first extraction is a routing change, and so is the rollback. Without it, the first extraction is a change to every caller, and the rollback is another change to every caller.
What makes the pattern valuable is not the shape but the property it guarantees: at every moment there is exactly one working system, and every step is reversible in the time it takes to flip a route. Compare that with the alternative people reach for — building the replacement alongside and cutting over when it is ready — where you maintain two systems, the old one keeps changing underneath the new one, and the entire risk arrives on one date.
Which piece to extract first
Not the hardest one, and not the most important one. The first extraction is a rehearsal: its job is to teach the team the pipeline, the deployment, the tracing, the on-call rotation and the data split, and to do that while the cost of getting it wrong is low. Pick something with few dependencies, a clear boundary, and low blast radius — notifications, PDF generation, image processing, a public read-only API.
From the second extraction onward, order by the constraint you named. And keep each one small enough that it lands within a few weeks — an extraction that takes six months has stopped being reversible in any practical sense, because the monolith it came from has moved on underneath it.
Splitting the data
Extracting the code is a week of work. Extracting the data is the migration, and it is where teams that skipped this section end up with two services on one database, which is the outcome the whole exercise was meant to avoid.
The sequence that works runs in the opposite order to the intuitive one. First separate the tables inside the existing database — same instance, different schema, no foreign keys crossing the line. This alone surfaces every hidden dependency, because every cross-boundary join now fails to compile or fails a test, and you fix them one at a time with the old system still running. Only when nothing crosses the boundary do you move the schema out to its own instance, and only then does the code move.
| Step | What it achieves | What it exposes |
|---|---|---|
| Separate schemas | One instance, two schemas, no cross-boundary keys or joins. | Every hidden coupling, cheaply |
| Route reads through the interface | Callers stop touching the tables and start calling a method. | Queries nobody knew existed |
| Write to both, read from old | The new store fills up and can be compared against the old one. | Where the two disagree, before it matters |
| Flip reads, keep writing both | Production traffic on the new store with a same-day way back. | Performance the test data never showed |
| Stop the old write | The split is real and the old tables can be dropped. | The one report still reading them |
Two techniques that do most of the work
The anti-corruption layer
The new service will need data from the old system, and the old system's model is usually shaped by fifteen years of accidents — a status column with eleven meanings, a customer record that is also a shipping address. If the new service speaks that model, it inherits it permanently and there was no point extracting it.
An anti-corruption layer is a translation boundary: a thin piece of code that speaks the legacy model on one side and the new service's model on the other, and belongs to the new service. It is deliberately ugly, deliberately isolated, and deliberately temporary — when the legacy source finally goes away, one component is deleted rather than a model being unpicked from every file.
Conway's law, used deliberately
Conway observed that a system's structure ends up mirroring the communication structure of the organisation that built it. In a migration this is not a curiosity — it is the strongest force in the room. If four teams share ownership of the extracted service, it will develop four internal factions and a coordination meeting, and it will not be independently deployable no matter what the diagram says.
Used deliberately — sometimes called the inverse Conway manoeuvre — this becomes a tool: change the team boundaries to the ones you want the services to have, and the architecture follows. It also sets the honest upper limit on the migration. You cannot end with more services than you have teams to own them, and a service nobody owns rots faster inside a distributed system than it ever did inside the monolith.
Knowing when to stop
Nothing in this pattern requires the monolith to disappear, and treating its disappearance as the goal is how a migration outlives its justification. Each extraction was justified by a constraint; when the remaining pieces are not blocking anything, the remaining extractions have no argument behind them except symmetry.
A permanent hybrid — three or four services around a well-modularised core that still holds most of the domain — is a common and perfectly respectable end state. It is also, in most organisations, the point of best return: the parts with a real independence requirement have it, and the parts without one are not paying for a network they did not need.
Measure the migration against the constraint you named, not against the count of services. If payments now releases twice a week instead of once a month, that extraction paid for itself and you can say so with a number. If the service count went from one to nine and lead time did not move, nine services is not progress — it is nine deployment pipelines.
How this shows up in real delivery
The migration competes with feature delivery for the same people, and that competition is decided by whoever is in the room when the quarter is planned. A migration run as a background activity by whoever has spare time takes three years and never finishes; one run as a total freeze on features loses the argument at the first commercial deadline. What survives is a stated share of capacity — a team, or a fixed fraction of every team's quarter — with each extraction landing as a visible result rather than as progress on a plan.
Where it degrades
- A big-bang rewrite alongside the running system, where all the risk arrives on one date and the old system keeps moving.
- Extracting the code and leaving the database shared, which produces a distributed monolith with extra steps.
- Starting with the hardest, most central component, so the team learns the mechanics where mistakes cost most.
- Extractions that run for months, which stop being reversible because the monolith has moved underneath them.
- Dual writes with no reconciliation, so the two stores drift apart silently.
- The new service speaking the legacy data model, which imports the mess it was meant to escape.
- Counting services as the measure of progress rather than the constraint that justified the work.
When to use it
Use it when
- You can name a specific constraint the current shape imposes, and measure it.
- Continuous deployment and tracing are already working, so an extracted service is operable on day one.
- There is a team ready to own the extracted service in production.
- Delivery can continue during the migration — a facade makes each step reversible.
- A permanent hybrid is an acceptable end state rather than a failure.
Avoid it when
- The reason is that the code is tangled — modularise inside the monolith first; it is cheaper and it is a prerequisite anyway.
- Nobody can be spared to own the new services, so they will be shared and therefore coupled.
- The plan is a rewrite with a cutover date rather than a sequence of reversible steps.
- The data cannot be split, because the domain genuinely requires transactional consistency across the boundary.
- The migration would be measured in services created rather than in the constraint removed.
Found this useful?
Share it with someone who is working on the same problem.