Complete system rewrites (“The Big Bang Rewrite”) have an astonishingly high failure rate in software engineering.
When a team decides to stop feature development on a legacy codebase and build a replacement platform from scratch, three things almost always happen:
- The rewrite takes 3x longer than estimated.
- The legacy application continues to change because business needs don’t pause.
- The new system misses subtle edge cases and bug fixes that were addressed in the legacy system over years of production usage.
The solution is incremental modernization—improving an existing system while it continues to run and generate business value.
1. The Strangler Fig Pattern
Named after vines that gradually grow around a tree until they replace it, the Strangler Fig Pattern allows you to replace legacy functionality route-by-route without taking down the existing system.
[ Incoming Traffic ]
│
▼
[ API Gateway / Proxy ]
│ │
├── (Old Routes) ─┼──► [ Legacy Monolith App ]
│ │
└── (New Routes) ─┴──► [ Modernized Microservice / Module ]
How to Implement
- Place an API Gateway or reverse proxy (Nginx, Traefik, Cloudflare Workers) in front of the legacy application.
- Build a new, clean implementation for a single high-priority endpoint (e.g.
/v2/orders). - Update the reverse proxy rule to route requests for
/v2/ordersto the new implementation while all other requests continue hitting the legacy app. - Repeat incrementally over time until the legacy app is retired naturally.
2. Characterization Testing: Establishing a Safety Net
Before modifying legacy code that lacks unit tests, write Characterization Tests (also called Golden Master tests).
A Characterization Test captures the current actual behavior of the legacy code—including its weird edge cases—so you can ensure your refactored code produces identical outputs.
test('Characterization Test: Legacy Tax Calculation', async () => {
const legacyOutput = await legacyCalculateTax({ amount: 100, state: 'CA', isExempt: false });
// Assert against captured production snapshot
expect(legacyOutput).toEqual({ tax: 8.25, total: 108.25 });
});
3. Dark Launching & Parallel Execution
When modernizing critical financial or inventory calculation pipelines, run the new implementation in Shadow Mode before switching primary execution.
async function processPayment(payload) {
// Primary execution: Legacy engine
const result = await legacyPaymentEngine.process(payload);
// Shadow execution: Fire-and-forget call to new engine (non-blocking)
newPaymentEngine.process(payload).then(newResult => {
if (JSON.stringify(result) !== JSON.stringify(newResult)) {
logger.warn({
message: 'Shadow Execution Mismatch Detected',
legacyResult: result,
newResult: newResult
});
}
}).catch(err => logger.error('Shadow execution error', err));
return result;
}
By logging output mismatches in shadow mode for a week, you can catch and resolve subtle calculation discrepancies before impacting a single live user.
Summary
Never sacrifice business continuity for clean code. By leveraging reverse proxies, characterization testing, shadow execution, and the Strangler Fig pattern, software engineering teams can eliminate technical debt safely without risking total system collapse.