Strangler Pattern: How I Modernize Monoliths Without a Big-Bang Rewrite
Why I avoid the big-bang rewrite
In older project/tender information and distribution-style platforms, the legacy system often contains years of undocumented rules.
A statement like:
we will rebuild everything in microservices in six months
usually underestimates:
- hidden validation;
- scheduled jobs;
- data cleanup rules;
- partner integrations;
- operational workarounds;
- historical reports.
I prefer Strangler migration when the old platform must continue running.
Step 1: Introduce a routing boundary
Client
|
Gateway / reverse proxy
|--------------------------\
| |
new Search Service Legacy Monolith
new User Service remaining routesNginx example:
location /api/search/ {
proxy_pass http://new-search-service;
}
location /api/users/ {
proxy_pass http://new-user-service;
}
location / {
proxy_pass http://legacy-monolith;
}Step 2: Choose a capability
I prefer a capability with:
- clear boundary;
- useful business value;
- limited cross-module write coupling;
- measurable output parity.
I do not start with the most dangerous financial/core workflow just to prove we can.
Step 3: Anti-Corruption Layer
Legacy models often have field names and assumptions I do not want in the new domain.
function toModernTender(
legacy: LegacyTenderRecord,
): Tender {
return {
id: String(legacy.TENDER_NO),
title: legacy.SUBJECT?.trim(),
closingAt: parseLegacyDate(
legacy.CLOSE_DT,
),
buyerId: String(legacy.ORG_ID),
};
}The adapter is the boundary.
The new service uses:
Tender
Buyer
ClosingDatenot every legacy column convention.
Step 4: Data ownership
The most dangerous transition is dual write:
legacy writes table
new service writes same tableI define one write owner per migration stage.
Temporary read-sharing can sometimes be accepted, but the end state should be explicit.
Options:
legacy remains write owner
-> new service receives events/CDC
new service becomes write owner
-> legacy calls new API or consumes replicated viewStep 5: Shadow comparison
Before cutting over a query, I can run:
real request -> legacy response
\-> new service response (not shown to user)Compare:
result count
business fields
latency
error rateThis is extremely useful for search/report migration.
Step 6: Percentage or cohort cutover
internal users -> new
5% traffic -> new
25% -> new
100% -> newI keep the old path available during controlled validation.
Step 7: Decommission
A Strangler migration fails if every old component remains forever.
I track:
capability
new owner
traffic cutover
data migration
old code disabled
old table/API decommissionedWhat I learned
The main risk is usually data, not HTTP routing.
If ownership remains unclear, the new service can look modern while the old database remains the real monolith.
Production checklist
- Inventory business capabilities.
- Introduce one stable routing boundary.
- Pick a low-risk first slice.
- Build an anti-corruption layer.
- Define one write owner.
- Compare old/new behavior.
- Cut over gradually.
- Preserve rollback.
- Monitor error/latency parity.
- Decommission migrated legacy code.
FAQ
Why not rewrite everything?
Because incremental migration reduces scope, keeps business running and exposes hidden rules earlier.
Can old and new systems share a DB?
Sometimes temporarily, but one write owner and an exit plan are essential.
What is the biggest Strangler risk?
Stopping halfway and operating a permanently coupled hybrid architecture.
Related architecture guides
- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects)
- [Adapter and Anti-Corruption Layer](/articles/adapter-anti-corruption-layer-external-supplier-integrations)
-
Architect's final take
The Strangler pattern gives me evidence after every migration step. That is more valuable than a beautiful target diagram with no safe route from the current system.
Your feedback helps prioritize deeper technical content.






