I once replaced a MuleSoft implementation with over 150 endpoints with a custom .NET solution running on Kubernetes in Azure. There was no version of "stage it, test it, flip the switch" that worked at that size. Too many routes, too many consumers who'd never agree to a maintenance window, too many edge cases nobody could enumerate up front. A big-bang cutover on 150 endpoints isn't a plan, it's a bet, and it's a bet you make once and then live with whatever the dice say.
So we didn't make that bet. We built a reverse proxy, pointed DNS at it instead of at MuleSoft, and let the new system prove itself in production, in the shadows, before it was ever allowed to actually answer a request.
The proxy sat in front of both implementations. Every request it received, it recorded — and it could route traffic to the old system or the new one by URI, by header, or by HTTP method, per route, independently. Nothing about the cutover had to be all-or-nothing; a single endpoint could be live on the new system while the other 149 were still safely on MuleSoft.
Alongside the proxy, we built a small web app whose only job was to diff the two systems' responses to the same requests, side by side. That diff was the actual decision-making tool. Not a test suite that says "should work" — a live comparison that says "here is everywhere they disagree, right now, on real traffic."
Here's the detail that took me a while to appreciate myself: we pointed DNS at the proxy just a few days after building it. Which meant we weren't building toward a cutover date. We were in shadow mode for the entire migration, from day two onward. We could push changes to the new implementation and reassess whether a given route was ready — every day, for as long as it took, instead of once at the end.
That's the same idea behind Week One: Go Live (Even If It Does Nothing), just applied to a replacement instead of a greenfield build: get into production early, while the stakes are still low, and let the real risk — whether the new thing actually behaves like the old thing — surface a little at a time instead of all at once.
The header comparison turned up something a functional test never would have: one MuleSoft endpoint was returning gzip-encoded responses, and the .NET candidate wasn't. Nothing about the response body was wrong. A consumer expecting compression would have just seen a different Content-Encoding header and, depending on how strict it was, possibly broken outright. That's the kind of mismatch that only shows up when you're comparing real responses to real requests, not when you're asserting against a fixture.
When the last endpoint's diffs went quiet — no more mismatches, day after day — we cut it over. That was it. There was no go-live event, no war room, no weekend deploy. We'd been live, in every sense that mattered, since day two. The only thing that changed on the last day was that the proxy stopped needing to ask MuleSoft anything at all.
I used the same approach later on a different project, modernizing a set of legacy SOAP web services, and it exposed a wrinkle worth mentioning. The proxy's URI/header/method routing worked because those requests actually varied across those dimensions. SOAP doesn't give you that — every operation goes out over HTTP POST, to the same endpoint, so there's no method or URI left to route on. The proxy had to look inside the request, at the SOAPAction header or the envelope body itself, to figure out which operation was actually being called before it could decide where to send it.
Same pattern, different protocol, and the protocol decided how much of the pattern you get for free. Worth knowing before you assume the routing logic that worked on one migration will just carry over to the next.