Supporting Legacy Clients in a Modern Serverless World: Lessons from a VS Code Extension
The client-server model never truly went away. It’s just evolved. In our case, we’re authoring a Visual Studio Code extension — a classic “thick client” in modern clothes — that communicates with an API deployed to Azure using Azure’s Serverless stack — Azure Functions. Even though the infrastructure is abstracted away by the cloud, the architectural challenges are still very much alive.
Our VS Code extension is distributed through client-side software, meaning users must manually install or update it. There’s no automatic upgrade process — no invisible push from our side. If we ship a new version of our server-side API and expect it to work with old clients, we have to guarantee backward compatibility. And that’s not just theoretical compatibility. It has to be tested, verified, and guaranteed before anything goes live.
The Problem with Fragmentation
When we deploy a new version of our API, we don’t just push fresh endpoints and hope for the best. We maintain both the old and new versions of the API simultaneously — say, v1 and v2—within the same Azure Functions app. The expectation is that the old version of the client will continue to operate against the v1 endpoints, while new clients will take advantage of v2.
Sounds simple, but here’s the rub: even though the API routes and endpoints technically stay the same, the new API version is a completely new artifact. New code. New workflows. Schema changes. Possibly even new business logic. We can’t just assume the old client will keep working just because its familiar endpoints are still exposed. We need to explicitly test old clients against the new API build.
The Reality of a Production Rollout
Lets explore this in a simplified use case. Let’s say, right now, in production, users are on version 0.9 of the client, talking to version 0.9 of the server. In our development environment, we’re working with client 1.0 and server 1.0. This newer pair is built to take advantage of all the new features: fresh endpoints, schema tweaks, upgraded logic — the works.
Before the Deployment
When we roll out server 1.0 to production, we enter a transition phase where users on the old 0.9 client will suddenly be talking to the new 1.0 server. And that’s a very real problem. Not everyone upgrades right away. Maybe someone was away from their desk. Maybe they went out for tacos. Either way, they missed the memo — and now the client they’re using is outdated.
After the Deployment
If we didn’t properly test the 0.9 client against the 1.0 server, these users could be in for a broken experience. That’s why our pre-deployment testing must cover both the current and legacy versions of the client. We can’t just focus on how the new client handles the new API — we need to verify that the old client gracefully continues to function with the newly deployed server code.
Backwards Compatibility Testing
Supporting Multiple Clients Is Costly
Over time, our user base fragments. Some users will upgrade, some won’t. That means we’re supporting multiple clients simultaneously — maybe not just two versions, but three or four, depending on how fast people update. And each supported version adds complexity to our testing and deployment cycles. The more versions we support, the more combinations we have to test. Every backward-compatible promise becomes a test case, and every test case is an opportunity for something to go wrong.
This kind of version fragmentation isn’t new. Android famously struggled with it for years. That platform had the additional challenge of being tightly coupled with hardware variations. At least we’re spared that headache. We benefit from multiple layers of abstraction: the OS abstracts away the hardware, and VS Code abstracts away the OS. Whether someone’s running Windows, macOS, or Linux doesn’t really matter to us. We’re targeting a runtime environment — VS Code — not the underlying system.
The (Relative) Simplicity of Our Stack
The simplicity of distributing a VS Code extension shouldn’t be underestimated. We don’t need to worry about GPU compatibility or kernel versions. We don’t have to deal with different device manufacturers or the quirks of custom Android builds. Instead, we rely on VS Code as a cross-platform runtime that handles those problems for us.
Still, the problem of version drift remains. Our extension is local software. If users don’t upgrade, they fall behind. So we need to balance our ability to innovate in the API with a commitment to keeping older clients functional. That’s why our testing process is structured around version compatibility, even if it means more effort up front. It’s the cost of maintaining a good user experience.
Conclusion
Supporting multiple client versions in a serverless, API-driven world isn’t a theoretical concern — it’s a practical necessity. When deploying a new server build, we can’t assume backward compatibility with our clients, be they a VS Code Extension or a native mobile app. We must test it. We must plan for it. And we must accept the reality that for a time, our user base will be split across versions.
The abstraction of Azure Functions may hide the “servers” from us, but it doesn’t erase the real, underlying complexity of versioned software. If we want our VS Code extension to keep working seamlessly for every user — whether they’re on version 0.9, 1.0, or beyond— we have to build and test like backwards compatibility really matters.
Because it does.