The shadow-proxy approach that made a 150-endpoint MuleSoft replacement boringly uneventful had one assumption baked into it that I didn't notice until the next project broke it: the proxy could decide where to send a request because the request itself told it what it was. Different endpoints had different URIs. Some operations only ever arrived as a GET, others as a POST. The traffic had shape, and the shape was free — the proxy didn't have to know anything about what an endpoint meant to route on it correctly.
Then I used the same pattern to modernize a set of legacy SOAP web services, and the shape was gone.
SOAP doesn't give you a menu of URIs or methods to route on. Every operation — twenty of them, forty of them, whatever the service exposed — goes out over HTTP POST, to the same endpoint. From the transport's point of view, "get a customer record" and "cancel an order" are the same request: same verb, same URL, same content type. The only thing that differs is what's inside.
That meant the routing logic I'd built for the MuleSoft migration — decide old-system-or-new by URI, header, or method, per route, independently — simply had nothing to grab onto. A proxy that only looks at the outside of the envelope can't tell these requests apart at all.
SOAP does leave you two ways to find out what's actually being called, and neither one is the free lunch that a REST URI is.
The first is the SOAPAction HTTP header, which exists specifically to say which operation a request is invoking, without anyone having to open the body. It's cheap to read and it's usually there. "Usually" is the catch — SOAPAction is optional in the spec, and plenty of real services either leave it blank or reuse a generic value across every operation, because whatever server framework generated their WSDL didn't bother populating it meaningfully. You can't build routing logic that assumes a header is trustworthy when the systems you're routing between disagree, silently, on whether to bother filling it in.
The second is the envelope body itself — the actual XML, with the operation name sitting somewhere inside as the first child element under the SOAP body. That's reliable; the operation has to be in there or the request doesn't mean anything at all. But it means the proxy can no longer stay ignorant of what it's forwarding. Making a routing decision now requires parsing XML on every single request, before the request has gone anywhere, which is real latency and real coupling added to a piece of infrastructure whose entire value proposition was supposed to be that it didn't need to understand the traffic to shepherd it safely.
So the proxy ended up doing both: check the header first since it's cheap and usually right, and fall through to opening the envelope when the header wasn't there or wasn't trustworthy enough to act on alone. Not elegant. Correct.
None of this broke the actual migration strategy. The Scientist-pattern shape — record everything, diff old against new, cut over per-route only once the diffs go quiet — worked exactly the same way it did on MuleSoft. What changed was the price of admission. On a REST API, the routing layer can be dumb and fast, because the transport already encodes the thing you need to know. On SOAP, the routing layer has to get smarter, and "smarter" here means slower and more tightly bound to a schema it would rather not care about.
That's worth knowing before the next migration, not during it. If the plan is "put a proxy in front and route by what the transport already tells you," the first question isn't whether the pattern applies — it's whether the transport is actually going to tell you anything. REST usually will. RPC-shaped protocols riding over a single verb to a single endpoint — SOAP, but it's not the only one — usually won't, and the routing logic has to be designed for that from the start instead of discovered halfway through wiring it up.