Definition
- Strangler-Fig Pattern – a facade routes traffic; initially all goes to legacy, then progressively more goes to the new service until legacy handles nothing and can be retired.
- Shadow Traffic – the facade sends real requests to both, returns the legacy response, and discards the new one after comparing.
- Parallel Validation – compares responses from both services and flags divergence before the new service is authoritative.
- Compatibility Layer – translates between old and new contracts so callers do not change in lockstep.
- Percentage Ramp – cut over by percentage (1%, 10%, 50%, 100%) watching metrics at each step.
- Return Path – a designed, fast, rehearsed way back to legacy for every increment (feature flag, routing rule).
National Bank of India wants to replace its decades-old transaction system but cannot switch everything overnight. So they put a receptionist (router) in front. At first every request goes to the old system. Then they quietly send just balance enquiries to the new system while everything else stays on the old one, double-checking answers. If it works, they move deposits, then transfers, one capability at a time, keeping the old system ready to take over instantly if the new one gives a wrong answer.
Understanding the Concepts
Why is a big-bang rewrite dangerous?
All risk is concentrated into a single moment with no gradual learning and often no safe way back.
package com.bank.migration;
public class BigBangRisk {
public static void main(String[] args) {
System.out.println("Big-bang rewrite:");
System.out.println(" Build new system for 6 months");
System.out.println(" Switch everything on one Saturday night");
System.out.println(" If something is wrong → no safe way back");
System.out.println(" All risk in one moment, no gradual learning");
System.out.println();
System.out.println("Strangler-fig:");
System.out.println(" Move one slice at a time");
System.out.println(" Validate each slice in parallel");
System.out.println(" Instant rollback for each increment");
System.out.println(" Risk spread across many small, reversible steps");
}
}
Big-bang rewrite: Build new system for 6 months Switch everything on one Saturday night If something is wrong → no safe way back All risk in one moment, no gradual learning Strangler-fig: Move one slice at a time Validate each slice in parallel Instant rollback for each increment Risk spread across many small, reversible steps
How does the strangler-fig facade work?
A routing facade sits in front. Initially everything goes to legacy. You progressively route slices to the new service.
package com.bank.migration;
public class StranglerFacade {
private final boolean balanceEnabled = true; // feature flag
private final boolean transferEnabled = false; // not migrated yet
String route(String capability, String request) {
if (capability.equals("balance") && balanceEnabled) {
return "NEW service: " + request;
}
return "LEGACY service: " + request;
}
public static void main(String[] args) {
StranglerFacade facade = new StranglerFacade();
System.out.println(facade.route("balance", "SAV2001"));
System.out.println(facade.route("transfer", "SAV2001 → SAV3002"));
}
}
NEW service: SAV2001 LEGACY service: SAV2001 → SAV3002
How does shadow traffic with parallel validation work?
Send real requests to both, return the legacy response, and compare the new response. Any divergence is logged for investigation.
package com.bank.migration;
public class ShadowTraffic {
static String legacyBalance(String accountId) {
return "75000.00";
}
static String newBalance(String accountId) {
return "75000.00"; // should match; log if different
}
public static void main(String[] args) {
String accountId = "SAV2001";
String legacyResp = legacyBalance(accountId);
String newResp = newBalance(accountId);
if (!legacyResp.equals(newResp)) {
System.out.println("DIVERGENCE on " + accountId
+ ": legacy=" + legacyResp + ", new=" + newResp);
} else {
System.out.println("Match on " + accountId + ": " + legacyResp);
}
System.out.println("Caller receives LEGACY response (safe). New response discarded.");
}
}
Match on SAV2001: 75000.00
Caller receives LEGACY response (safe). New response discarded.
How do you cut over by percentage?
Ramp gradually: 1%, 10%, 50%, 100%, watching metrics at each step.
package com.bank.migration;
import java.util.concurrent.ThreadLocalRandom;
public class PercentageRamp {
static String route(String request, int newServicePercent) {
int roll = ThreadLocalRandom.current().nextInt(100);
if (roll < newServicePercent) {
return "NEW service: " + request;
}
return "LEGACY service: " + request;
}
public static void main(String[] args) {
int percent = 10; // 10% to new service
for (int i = 0; i < 5; i++) {
System.out.println(route("balance SAV2001", percent));
}
System.out.println("At " + percent + "% ramp, watch error rate and latency");
}
}
LEGACY service: balance SAV2001
LEGACY service: balance SAV2001
NEW service: balance SAV2001
LEGACY service: balance SAV2001
LEGACY service: balance SAV2001
At 10% ramp, watch error rate and latency
Why must every increment include a return path?
Incremental migration is only safe if each step is reversible. A feature flag or routing rule must be flippable instantly.
package com.bank.migration;
public class ReturnPath {
public static void main(String[] args) {
System.out.println("Before cutover: test the rollback");
System.out.println(" 1. Route balance to NEW (feature flag ON)");
System.out.println(" 2. Observe for 1 hour");
System.out.println(" 3. Bug found! Flip feature flag OFF");
System.out.println(" 4. Balance routes back to LEGACY instantly");
System.out.println(" 5. No data lost because both read from shared store");
System.out.println();
System.out.println("If there is no designed return path, it is a big-bang in disguise.");
}
}
Before cutover: test the rollback
1. Route balance to NEW (feature flag ON)
2. Observe for 1 hour
3. Bug found! Flip feature flag OFF
4. Balance routes back to LEGACY instantly
5. No data lost because both read from shared store
If there is no designed return path, it is a big-bang in disguise.
How do you keep legacy data current during migration?
During migration, writes often must go to a shared source of truth or be dual-written, so flipping back never loses data.
package com.bank.migration;
public class SharedSourceOfTruth {
public static void main(String[] args) {
System.out.println("Option A: Shared database (both old and new read/write same DB)");
System.out.println(" Rollback: flip route, data is always current");
System.out.println();
System.out.println("Option B: Dual-write (write to both old and new stores)");
System.out.println(" Rollback: stop writing to new, old store is current");
System.out.println();
System.out.println("The guarantee: flipping back NEVER loses data");
}
}
Option A: Shared database (both old and new read/write same DB)
Rollback: flip route, data is always current
Option B: Dual-write (write to both old and new stores)
Rollback: stop writing to new, old store is current
The guarantee: flipping back NEVER loses data
What are common mistakes?
Discovering mid-incident that there is no safe way back, which is exactly the failure the strangler-fig exists to prevent.
package com.bank.migration;
// WRONG: migrate to new service, decommission legacy database
// Bug found → no way back, data only in new store
// CORRECT: keep legacy fully operational until migration is COMPLETE
// Test rollback BEFORE cutover, not during an incident
What are common interview follow-up questions?
- How do you handle writes during the migration window?
- What if the legacy service's API is undocumented?
- How do you decide the order of capabilities to migrate?
- When is it safe to decommission the legacy service?
Visual Overview
Strangler-Fig Migration
───────────────────────
Phase 1: Shadow
Client ──► Facade ──► Legacy (response returned)
└──► New (response compared, discarded)
Phase 2: Ramp by percentage
Client ──► Facade ──► Legacy (90%)
└──► New (10%, observed)
Phase 3: Full cutover
Client ──► Facade ──► New (100%)
Legacy (standby, ready for rollback)
Phase 4: Retire
Client ──► Facade ──► New (100%)
Legacy decommissioned
At EVERY phase: flip feature flag → instant rollback to Legacy
Key Takeaway
Never replace a legacy service with a big-bang rewrite. The strangler-fig pattern spreads the risk across many small, reversible steps. A routing facade sits in front of legacy, and you move one slice at a time: shadow traffic first to compare, then percentage ramp to build confidence, then full cutover.
Before trusting the new service, de-risk with shadow traffic and parallel validation. Send real requests to both, return the legacy response, and flag any divergence. Only when divergence is zero do you cut over, usually ramping by percentage with a compatibility layer so callers do not change in lockstep.
Every increment must include a designed, fast, rehearsed return path. A feature flag or routing rule must be flippable instantly, and the legacy must remain functional with current data so flipping back never loses data. Test the rollback before the cutover, not during an incident.
Remember one sentence: a legacy replacement succeeds when every increment is small, validated in parallel, and individually reversible, so the organization can always walk back the moment the new path misbehaves.
---