A safer migration plan for CTOs and platform leads, walked through with a hypothetical account-notification service. It is an incremental legacy modernization approach. You build a new implementation of one business capability beside the old one, route traffic to it in controlled steps, compare behavior against the legacy path, and retire the old path only after the new one is proven.
Why incremental legacy modernization beats the big-bang rewrite
Most enterprises carry at least one system that nobody wants to touch and nobody can replace overnight. It runs billing, orders or notifications, and it works well enough that the business depends on it.
A full rewrite looks tidy on a slide. In practice it hides risk until the cut-over date, when every undocumented behavior surfaces at once.
Martin Fowler described the alternative in 2004 after seeing strangler figs in the rainforests of Queensland. His suggestion was to “gradually create a new system around the edges of the old.” He also named the main reason to prefer this route over a cut-over rewrite: reduced risk. He noted that rewrites are always more complex than they look, because even old bugs often have to be carried across.
That is the heart of a sound legacy modernization strategy. You shrink the unit of change until each step is small enough to reverse.
What is the strangler fig pattern in three moves?
The pattern has three moves that repeat for every capability:
Transform: build the new implementation of one capability.
Coexist: run old and new together behind a routing layer.
Eliminate: remove the old path once the new one has earned trust.
The rest of this article applies those moves to one concrete case, so you can reuse the plan for your own legacy system migration strategy.
The example: a hypothetical account-notification service
Picture a financial platform whose monolith sends account notifications: password resets, failed-payment alerts, low-balance warnings and “statement ready” messages. Email, SMS and push logic all live inside the monolith and read shared database tables.
This is a hypothetical scenario built for illustration. It does not describe a specific client, and the numbers you choose will depend on your own systems and risk appetite.
Notifications make a good first capability. They are customer-visible, so mistakes get noticed. They are also event-driven, so inputs and outputs are easy to observe and compare. That balance is what you want in a first slice.
Step 1: Draw the boundary and build the seam
Define the capability in one sentence
Write it down: given an account event, decide who is notified, on which channel and with what content, then record the outcome. Everything inside that sentence moves. Everything outside it stays.
For our example, authentication and customer profile ownership stay in the monolith. The new service reads preferences through an API instead of reaching into shared tables.
Write the contract before the code
Agree on three things with the teams that depend on you:
Input: an account event with a unique event ID.
Output: a message request plus a delivery record.
Guarantee: the same event ID never produces two sends.
Add a routing seam first
Put a facade, API gateway or event router in front of the notification calls. At the start it sends 100 percent of traffic to the legacy path. This changes no behavior, but it gives you the control point every later step depends on.
Step 2: Plan coexistence, because both systems will run for a while
Coexistence is where most migrations get hurt, so give it real design time.
One system of record per data set. At any moment, exactly one side owns notification preferences. Avoid dual writes. Publish changes through an event stream or an outbox table so the other side follows.
Idempotency everywhere. If both paths can see the same event, a customer could get two texts. A shared event ID and a send ledger prevent that.
Shadow before live. In shadow mode, the new service receives a copy of each event and decides what it would send, but sends nothing. You get production-shaped data with zero customer impact.
Step 3: Route traffic with a canary deployment strategy
A canary deployment strategy moves traffic to the new path in small, observable steps. For a capability migration, route by account cohort, not by random request. A customer should not get half their alerts from one system and half from the other.
Use the simulator below. Drag the slider through an illustrative rollout and see what to check at each stage.
Traffic routing simulator
ShadowInternalSmallLargerHalfFull
Shadow mode · 0% live
What to check: The new service gets copies of events and computes decisions. Nothing is sent. Compare every decision with the legacy result.
Canary deployment vs blue green
Teams often ask about canary deployment vs blue green. They solve different problems, and a migration usually uses both.
| Canary | Blue green | |
| How it shifts traffic | Gradually, by cohort | All at once, between two full environments |
| Rollback | Reduce the routed share | Switch back to the old environment |
| Exposure if wrong | Limited to the canary cohort | Everyone, until you switch back |
| Best use here | Moving a capability from legacy to new | Releasing new versions of the new service |
Step 4: Compare behavior before you trust it
Passing tests does not prove the new path matches production. Comparing real decisions does.
For every event in shadow mode, store what the legacy path decided and what the new service would decide. Compare meaningful fields, not raw bytes: recipient, channel, template, and timing.
Then sort each mismatch into one of three buckets:
New service bug: fix it.
Legacy bug customers rely on: decide on purpose whether to copy it or change it, and tell the business.
Acceptable difference: document it and move on.
Agree on pass criteria with the capability owners before the rollout starts. Useful signals include match rate, delivery latency, failure rate, duplicate sends and support contacts. Choosing thresholds in advance stops the team from moving the goalposts under deadline pressure.
Step 5: Retire the old path on purpose
Old code that stays “just in case” keeps costing you. It still needs patches, infrastructure and on-call knowledge, and it quietly keeps the old risk alive.
Keep the legacy path warm while the new one runs at full traffic through at least one complete business cycle, such as a statement run. Then work through the list below.
Retirement checklist
New path has carried 100% of traffic through a full business cycle
Behavior comparison shows no unexplained mismatches
Changes to the legacy notification code are frozen
Rollback window has ended and the owner has signed off
Routing fallback to the legacy path is removed
Historical data is archived or migrated
Legacy code, jobs and infrastructure are deleted
Runbooks, dashboards and alerts point only to the new service
0 of 8 complete
Common legacy modernization challenges, and how to handle them
Shared databases. Replace direct table access with an API or events before moving logic.
Scope creep. The new service should match old behavior first. Improvements come after the cut-over.
Parallel-run fatigue. Set an end date for coexistence at the start. Open-ended dual running drains teams.
Undocumented behavior. Shadow comparison finds it. Interviews rarely do.
No clear owner. Name one accountable owner per capability, with authority to approve each routing step.
Choosing your next capability
Pick slices that are well bounded, observable, and low enough in blast radius to teach the team. Notifications, document generation and reporting often qualify. Core ledgers and pricing engines usually come later, once the routing, comparison and retirement habits are proven.
A good application modernization program is a series of small, boring, reversible wins. That is the point.
Frequently asked questions
What is the strangler fig pattern?
It is an incremental way to replace a legacy system. You build new functionality beside the old, route traffic gradually, and remove the old code once it is no longer used.
How long does incremental legacy modernization take?
It depends on how many capabilities you move and how tangled their data is. Plan in slices, and judge progress by capabilities retired, not by calendar dates.
When should you not use the strangler fig pattern?
Skip it when the system is small enough to rewrite safely, or when no clean seam exists to route traffic through. In the second case, create the seam first.
Should we use canary or blue green deployment?
Use canary routing to move a capability from legacy to new. Use blue green when releasing versions of the new service itself.
What is a good first capability?
One with clear inputs and outputs, an accountable owner, and observable results. Notification services are a common choice.