DevelopmentSenior

Monolith → Microservices

Migrating a monolith to microservices means moving an existing system’s capabilities into separately deployed services one at a time, without stopping the system. The migration that works is not a rewrite with a cutover date but a sequence of small extractions behind a facade, each justified by a measured constraint and each with a way back; the hard part is the data. The page opens with one extraction as sixteen steps, then covers what to extract first, six ways to move code without stopping, splitting the data, the model and the teams, running the migration and knowing when to stop, five documented cases, and where it degrades.

Pattern
Strangler fig (Fowler, 2004)
Unit of work
One extraction, with a way back at each step
Hardest part
Splitting the data

One extraction, start to finish — sixteen steps, how you know each is done, and the way back

A migration that works is not one big project. It is the same small project repeated: take one capability out of the monolith and put it into a service, with the system running the whole time. Each repetition has the same sixteen steps, in the same order. The column that matters most is the last one. It says how to undo each step, and for almost all of them the answer takes minutes. Later sections cite the steps by number.

#StepDone whenThe way back
1Name the constraint and measure it — “payments waits 21 days for unrelated releases”A number, a target and a date are written downNothing to undo. If there is no number, stop here: the work is modules, not services
2Choose the capability to extractIts job fits in one sentence without “and”, and few modules call itChoose another — nothing has moved yet
3Stand up the platform minimum: pipeline, tracing, logs, alerts, an on-call rotaAn empty service reaches production through the pipeline and shows up in a traceNothing user-facing has changed
4Put a facade in front of the monolith that forwards everything unchangedAll traffic passes through it, with errors and latency unchangedPoint traffic back at the monolith
5Draw the module boundary inside the monolith: one public interfaceA CI check fails on any new import of the module’s internalsA revert — it is still one deployable
6Give the module its own schema: no joins or foreign keys across the lineA query check finds no SQL that crosses the boundaryA revert — it is still one database
7Route every access through the interface, reports and batch jobs includedNothing outside the module touches its tablesA revert
8Make the module’s tests run on their ownIts suite passes with nothing else loadedNothing to undo
9Build the service with its own store; backfill it and keep it in sync from the monolith’s database logA reconciliation job reports no drift for a weekSwitch the sync off — nothing reads the copy yet
10Parallel run: send reads to both, serve the monolith’s answer, log every mismatchNo mismatches for a full business cycle — a week, a month endStop the experiment
11Move reads to the service — 1%, 10%, 50%, 100% — behind a flagErrors and latency stay at baseline with every read on the serviceFlip the flag — the monolith’s data is still current
12Move writes: the service becomes the source of truth and syncs changes back to the old tablesEvery write goes through the service, and the reverse sync shows no driftFlip writes back — the old tables are current, so the monolith can take over again
13Point every remaining reader — reports, exports, other modules — at the service or an analytics copyQuery logs show no reads of the old tables for a monthPoint the reader back
14Stop the reverse sync and drop the old tables, after a backupThe tables are gone and nothing brokeRestore from the backup — the first step with no quick way back
15Delete the module’s code from the monolith and the old route from the facadeThe code is goneA revert and a redeploy — no longer a flag
16Measure the constraint from step 1 against its baseline, and decide what comes nextThe number is published, and the next extraction is chosen — or the migration stops—
The facade goes in first and never changes shape — only its routing table does. Behind it, functionality moves from the old system to new services one route at a time. Every intermediate state is a working, shippable system, and every step can be reversed by pointing one route back.

Before the first extraction: the reason and the candidate

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, a monolith that still holds the important logic, and a team slower than when it started. The ordering that works begins with step 1: a named constraint you can measure today. Each reason people give for a migration either passes that test or points at cheaper work, and the table sorts the common ones.

The reason givenDoes it justify a service?Check first
“The code is a mess”No — a split follows the coupling that already existsModule boundaries — rows 1–7 on the Monolith page
“Deploys are slow and scary”No — deploy frequency is not an architecture propertyThe pipeline, smaller batches, feature flags — rows 8–12 there
“One part needs far more CPU or memory”SometimesA separate process pool started from the same build
“One part needs another language or runtime”Yes, if the gain is measured rather than preferredA library, or a small helper process
“Card data must leave the audit scope”YesWhether the auditor accepts a separate process and database instead
“Teams own clean modules and still block each other at release”Yes — the case the style exists forBatch size and flags, to be sure the wait is not just big releases
“The platform underneath is end of life”It forces a move, not an architectureWhether a port inside the monolith is cheaper
“Everyone else has microservices”NoWhat the measured constraint is

Step 2 picks the first candidate, and it should be neither the hardest piece nor the most important one. The first extraction is a rehearsal. Its job is to teach the team the pipeline, the tracing, the on-call rota and the data handover while a mistake is still cheap. So it wants few callers, a clear boundary and a small if it fails. The table scores the usual candidates.

CandidateWho calls itHow tangled its data isIf it breaksVerdict
NotificationsMany modules, in one directionIts own templates and send logMessages arrive lateFirst — the classic rehearsal
Image processingUploads onlyFiles, not tablesUploads are slowerFirst — often the scaling reason too
PDF and report generationA few screensReads other modules’ dataDocuments arrive lateEarly, once it reads through interfaces
SearchMost screensA copy of others’ data, already fed by changesResults go staleEarly — it is a read model already
AuthenticationEvery requestThe users table sits at the centreNobody can sign inLater — once the platform is proven
PaymentsCheckout and refundsMoney, with strict consistencyMoney lost or taken twiceLater — often for compliance
Orders, the core domainEverythingJoined to everythingThe business stopsLast, or never

From the second extraction on, order by the constraint from step 1, not by convenience. And keep each one small enough to land within a few weeks. An extraction that runs for six months has stopped being reversible in any practical sense, because the monolith it came from has moved on underneath it.

Six ways to move code without stopping the system

The best-known pattern has a botanical name. During a holiday in the Queensland rain forests in 2001, Martin Fowler saw strangler figs. The fig starts as a seed in a nook of a host tree, grows down to the ground and up to the light, and can end up standing on its own where the host tree was. A couple of years later he used it as a picture of how his colleagues replaced legacy systems, in a short post called “Strangler Application”. He later renamed it “Strangler Fig Application”, because people kept the violent word and forgot the plant. In August 2024 he rewrote the post, and the new version names four activities that Ian Cartwright, Rob Horn and James Lewis defined for any gradual replacement. Understand the outcomes you want. Decide how to break the problem into smaller parts. Deliver the parts successfully. And change the organisation so that this can go on.

The same post makes an argument teams often resist. A gradual replacement needs transitional architecture: facades, sync jobs and switches that exist only while old and new run side by side, and are deleted afterwards. It looks like waste. Fowler’s answer is that the reduced risk and the earlier value outweigh its cost. Steps 4, 9 and 12 are exactly this kind of code. Budget for them instead of resenting them.

The facade goes in first, before anything is extracted (step 4). With the facade in place, the first extraction is a routing change, and so is its rollback. Without it, the first extraction is a change to every caller, and the rollback is another change to every caller. Sam Newman’s “Monolith to Microservices” (2019) collects the strangler fig and five related patterns for moving code, and each fits a different situation:

PatternHow it worksUse it whenWhat it costs
Strangler fig applicationA facade sends each route to the monolith or to the new serviceThe capability is reached from outside — a URL, a queue, an API callA proxy to run; it only sees what crosses the edge
UI compositionWhole pages, or parts of a page, are served by the new serviceThe capability has its own screensTwo front-end stacks for as long as it lasts
Branch by abstractionInside the monolith, an interface lets old and new implementations coexist behind a switchThe capability is called from deep inside the codeAn abstraction layer to add, and later to remove
Parallel runBoth implementations handle the same request; the old answer is served and the two are comparedCorrectness matters and is hard to specifyDouble the work, and side effects must be kept out of the new path
Decorating collaboratorA proxy passes the request to the monolith unchanged, then triggers new behaviour in the service from the responseNew behaviour is added without touching the monolith — points earned on every orderWorks only if the response carries enough data
Change data captureThe new service reacts to changes read from the monolith’s database logNothing in the monolith can be changed or calledIt couples the service to the monolith’s schema

Branch by abstraction: a seam inside the code

A facade only sees what crosses the edge of the system. Much of what you want to extract is called from inside it — a pricing function used by checkout, invoices and the admin pages. Branch by abstraction handles that case. Paul Hammant gave the technique its name while arguing for , crediting Stacy Curl with the idea, and Fowler described the steps in January 2014. Put an interface in front of the old code. Move every caller onto the interface, one at a time. Build the new implementation behind the same interface — here, a client that calls the new service. Switch callers over with a flag. When nothing uses the old implementation, delete it, and later the interface too if it no longer earns its place. Every commit along the way keeps the system building and releasable, so no long-lived branch ever has to be merged.

Branch by abstraction moves code that is called from deep inside the monolith, where a facade at the edge cannot reach. First an interface is put in front of the old implementation and every caller is moved onto it; then a new implementation — here a client of the new service — is built behind the same interface, and a flag decides which one runs; finally the old implementation is deleted, and later the interface if it no longer earns its place. Every one of these states is committed to the main branch and can be released, so the change never lives on a long-lived branch that has to be merged.

Parallel run: proving the new code before trusting it

A rewrite is only as good as its fidelity to the old behaviour, and the old behaviour is rarely written down. A parallel run (step 10) measures it instead of guessing. Every request, or a sample of them, goes to both implementations. The user gets the old answer. The new answer is compared with it, and every difference is recorded with enough context to debug. GitHub built a library for this, Scientist, and described using it to replace the code that merges . After four days of fixing what the experiment found — including a feature missing from the new merge engine altogether — the new code ran for 100% of requests for 24 hours with no mismatches and no slow cases. Tens of millions of merges had been checked before the old path was switched off. Khan Academy did the same at the edge of its system and called it side-by-side testing: its gateway sent most traffic for a request to the monolith and some to the new services, and compared the results until every difference was fixed.

A parallel run sends the same request to both implementations but returns only the old one’s answer to the user. The new answer is compared with it, and every difference is recorded with enough context to debug, so the new code is proven against real traffic before anyone depends on it. The experiment starts on a sample of requests to catch crashes, grows to every request to catch the rare cases, and the switch happens only once the mismatch log stays empty for a full business cycle. The new path must not perform side effects, or the experiment doubles the action it was meant to observe.

Splitting the data — the migration itself

Extracting the code is a week of work. Extracting the data is the migration. It is also where teams that skipped this section end up with two services on one database — the outcome the whole exercise was meant to avoid. Newman describes three orders: split the database first, split the code first, or do both at once. He warns against doing both at once. In his experience code first is the most common, because it shows results sooner. Its usual failure in practice is stopping there, with the new service still reading the monolith’s tables.

The sixteen steps avoid both traps by moving one thing at a time. The split starts inside the monolith, where it is cheap (steps 6–7). Separate the module’s tables logically — same database, its own schema, no joins or across the line. This alone surfaces every hidden dependency, because every crossing now fails a check, and each one is fixed while the old system keeps running. Then the data is copied and kept in sync (9), reads move (10–11), writes move (12), the last readers move (13), and only then is anything deleted (14).

GitHub ran exactly this at scale between 2019 and 2021, on the database cluster that held users, repositories, issues and pull requests. First came virtual partitions. Tables that belong together were grouped into schema domains, listed in a plain YAML file in the Rails application. Two linters enforced the groups: one failed any query that referenced tables from two domains, the other any transaction that spanned them. Existing violations were annotated as exemptions, which turned them into a visible backlog. A domain with no exemptions left was ready to move. The physical move used Vitess, or a custom cutover over MySQL replication that switched writes in a few tens of milliseconds, at the quietest hour of the day; one cutover moved 130 of the busiest tables at once. In 2019 the cluster answered 950,000 queries a second. In 2021 the same tables, spread over several clusters, answered 1,200,000 — and the average load on each host had halved.

While a capability’s data moves, only one thing changes at a time. First the service gets its own copy, filled and kept in sync from the monolith’s database log and compared against it. Then reads move to the service while the monolith still takes the writes and remains the source of truth. Then writes move: the service becomes the source of truth and syncs its changes back, so the monolith could take over again at any moment. Only after every remaining reader has moved are the old tables dropped — the one step whose way back is a restore from backup rather than a flag.

Not every database splits along clean lines, and Newman’s book names the patterns for the awkward cases. These seven come up in almost every migration:

PatternWhat it doesWhen it fitsThe catch
Database viewThe new service reads the monolith’s tables through a view that hides their layoutA read-only need, early onStill coupled to the monolith’s database
Database wrapping serviceA thin service in front of tables nobody can untangle yetCallers must stop reaching the tables directlyThe mess is hidden, not removed
Change data ownershipThe service becomes the owner; the monolith calls it instead of the tablesThe capability has moved and its data should follow (step 12)Every monolith query on those tables must be rewritten
Synchronize data in applicationThe application writes to both stores while the move runsNo database log is available to sync fromTwo writes are not a transaction — reconcile, or drift
Tracer writeThe new store becomes the source of truth for one slice of data at a timeThe data is too large or too busy to move in one stepTwo sources of truth until the last slice moves
Split tableA table whose columns belong to two capabilities becomes two tablesOne “customers” table holds both account and shipping dataEvery query that read both halves changes
Move foreign-key relationship to codeThe join becomes a lookup through the other service; integrity is checked in codeEvery cross-boundary key found in step 6Slower reads, and orphans the database no longer prevents

Reports are where a data split is usually found to be unfinished (step 13). A report that joins six modules’ tables worked for years because everything sat in one database. After the split there are two honest answers. Either the report calls the services that own the data, or it reads an analytics copy fed by events or by the database logs. Newman recommends a schema built for reporting rather than queries against the operational stores. Either way, find every reader before step 14. The one monthly export nobody remembered is a classic cause of a rollback on the first of the month.

The model and the teams: the anti-corruption layer and Conway’s law

The new service will need data from the old system, and the old system’s model is usually shaped by fifteen years of accidents. If the new service speaks that model, it inherits it permanently, and there was little point extracting it. Eric Evans named the defence in “Domain-Driven Design” in 2003: an anti-corruption layer. It 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, instead of a model being unpicked from every file. Here is what it typically translates:

In the legacy modelWhat the layer doesIn the new service
A status column with eleven meanings, some set only by one batch jobMaps each code to a named state, and rejects the codes it does not understandExplicit states with the transitions allowed between them
A customer record that is also the shipping addressSplits it into two conceptsA customer, and the addresses they ship to
Times stored as local-time stringsParses them with the server’s historical time zoneUTC timestamps
Amounts stored as floating-point numbersRounds once, explicitly, and logs what it roundedInteger minor units plus a currency code

Melvin Conway observed in 1968 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 grow four internal factions and a coordination meeting, and it will not be independently deployable whatever the diagram says. Used deliberately, the same force becomes a tool. Jonny LeRoy and Matt Simons named it the inverse Conway maneuver in 2010: 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. So step 3 includes an on-call rota on purpose. The owning team runs the new service from the day it takes its first request — Amazon’s rule, in Werner Vogels’s words, “You build it, you run it.”

Running the migration: capacity, pace, and when to stop

The migration competes with feature work for the same people, and that competition is decided by whoever is in the room when the quarter is planned. A migration run in the background by whoever has spare time takes three years and never finishes. A migration run as a total freeze on features loses the argument at the first commercial deadline. What survives is a stated share of capacity — one 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.

Stop the old code growing while you move it. Khan Academy’s rule during its rewrite was that every new feature written in the old Python code needed the personal approval of its head of engineering, because every new line was one more line to port. The same project measured its progress by what had moved, not by what had been built: the share of traffic served by the new backend, the number of fields completed, and the scope that grew as the work was understood. The table lists the measures that tell a working migration from a stalled one.

MeasureHealthy signWarning sign
The constraint from step 1Moves within weeks of each extractionUnchanged after the third extraction
Lead time in the extracted areaShorter than beforeLonger — changes now wait for two pipelines
Features that touch the monolith and a service togetherRare, and getting rarerMost features
Share of traffic on the new pathRises step by stepStuck at the same share for months
Services per owning teamA fewMore services than people
Incidents that cross the boundaryFallingRising with each extraction

Nothing in the 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 — a few services around a well-modularised core that still holds most of the domain — is a common and respectable end state. In most organisations it is also the point of best return: the parts with a real need for independence have it, and the rest are not paying for a network they never needed. SoundCloud’s engineers said as much in 2015: some of what the monolith did was so small and stable that it might be cheaper to keep it there forever.

Measure the migration against the constraint you named in step 1, not against the number of services. If payments now ships 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 are not progress. They are nine deployment pipelines.

Five cases, and what each one was missing

The five cases span twenty years, from the migration that shaped the word to one that finished in 2021. Each is told by the people who did the work, and each shows a different step of the sixteen carrying the weight.

CaseWhat happenedWhat it shows
Amazon, 2001–2006The Obidos monolith could no longer scale; shared databases had left no isolation and no clear ownerThe rule that came out of it was about data and ownership, not size
Netflix, 2008–2016A database corruption stopped DVD shipping for three days; seven years later the last data-centre pieces were shut downBilling and customer data moved last, and that took years on its own (steps 12–14)
SoundCloud, 2012–2015A value-stream map showed features took two months, mostly waiting; new features went to services, old ones stayed slowMeasuring the constraint first (step 1) — and that new services alone do not fix old code
GitHub, 2015–2021Rewrites proven by parallel runs; the main database split by schema domains and linters, then movedLogical separation before physical (steps 6, 10)
Khan Academy, 2019–2021A Python 2 monolith ported to 26 Go services; 95% of traffic on the new backend by 30 August 2021A forced move done incrementally, with side-by-side testing and a freeze on old-code growth

Amazon’s is the story the vocabulary came from. In a 2006 interview Werner Vogels described Obidos, the application that had held all of Amazon’s business and display logic since the company started. For years the scaling effort went into the databases behind it, until in 2001 the front end itself could no longer scale. The deeper problem was sharing. The databases were shared by many teams and processes, so nothing had isolation and nothing had a clear owner, and both the front end and the back end were stuck. The answer was a service-oriented architecture with one rule above the others: data is encapsulated with the logic that operates on it, reachable only through the service’s published interface, with no direct database access from outside. It grew into hundreds of services. Read the rule again and notice what it does not mention. It says nothing about how small a service should be. The constraint was ownership, and so was the fix.

Netflix started in August 2008, when a major database corruption meant it could not ship DVDs for three days. The lesson it drew was to move away from vertically scaled single points of failure such as one relational database in its own data centre. It chose not to lift the existing systems into the cloud unchanged, because that would have moved all the old problems with them. Instead it rebuilt: from a monolithic application to hundreds of microservices, with a denormalised data model on stores. Every customer-facing system had moved before 2015. Billing and customer data took the time after that, and the last data-centre pieces were shut down in early January 2016 — seven years after the start. The part that took longest was not the services. It was the data with the strictest rules attached to it.

Where it degrades

A migration rarely fails on one day. It fails by stalling: half the steps done for many capabilities, and all the steps done for none. Almost every stalled migration shows several of these:

  • A big-bang rewrite beside the running system, where all the risk arrives on one date and the old system keeps moving.
  • No named constraint, so success is counted in services created (step 1).
  • Code extracted and the database left shared — a with extra steps (steps 9–14 skipped).
  • Starting with the hardest, most central capability, so the team learns the mechanics where mistakes cost most (step 2).
  • No facade, so every extraction and every rollback is a change to every caller (step 4).
  • Extractions that run for months, which stop being reversible because the monolith moves underneath them.
  • Dual writes with no reconciliation, so the two stores drift apart silently.
  • A parallel run that also performs the side effects, doubling what it was meant to observe (step 10).
  • The new service speaking the legacy data model, which imports the mess it was meant to escape.
  • Old tables dropped before every reader has moved, found by the monthly report (steps 13–14).
  • New features still landing in the old code, so the work to port grows faster than the port.

The honest limit is that the method makes a migration safe, not short. Every step on this page exists to keep the system working and the way back open, and each of them costs time: a facade, a sync, a parallel run, a month of logs before a table is dropped. Netflix took seven years. A team that needs the result by a fixed date cannot buy it with a better pattern, only with a smaller scope. Most of the time the right scope is smaller than the plan — a handful of extractions, each paying for itself, around a monolith that stays.

The practical check takes an afternoon. Ask for the constraint behind the migration and its number today. List the extractions in progress and the step each one has reached; more than two in progress at once is a warning. Check whether a facade sits in front of the monolith. For each extracted service, find out whether its old tables still exist, and who still reads them. Search for code that writes to two stores in a row. Count the new features added to the monolith last quarter. And for the next extraction, write down its way back before writing any code.

When to use it

Use it when

  • You can name a specific constraint the current shape imposes, and measure it.
  • Automated deployment and tracing already work, so an extracted service can be run from its first day.
  • There is a team ready to own the extracted service in production.
  • Delivery has to continue during the migration — a facade keeps every step reversible.
  • A permanent hybrid is an acceptable end state rather than a failure.

Avoid it when

  • The reason is tangled code — make modules inside the monolith first; it is cheaper and 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 needs transactional consistency across the boundary.
  • The migration would be measured in services created rather than in the constraint removed.

Sources and method

What this page stands on

  • 14 sources
  • Checked October 13, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.