The idea is to link a thin client library into the user’s code.
Then the thick client is controlled server side by the devs.
If you design the thin and thick client intelligently, you can make a lot of changes to the system by merely modifying the thick client (which you control), without needing to update the thin client.
Problems tends to come in a few flavors, namely that this increases complexity and that some changes will always require modifying the thin client.
The biggest pitfall, though, is that you have to actually put thought into the thin client design. “Thin” is an emergent property of a well thought out client. You can’t just “touch thin_client.java” and think because you named it “thin” that it’s inherently decoupled from the service. You have to actually put thought into it.
Your example is exactly right. The service can support either v1 or v2 depending on the version of the thin client.
The main other technique is to have the thin client communicate in a relatively abstract and generic manner. For instead a “write()” endpoint that accepts an “options” dictionary.
“write()” is so generic that it probably won’t have to change, and changes in behavior can be modified by shoving stuff into dict, perhaps with a “version” field to instruct the server how to interpret the call.
When you add a new feature, you can bump the thin client to v2 and shove new options in the dict. Then the server can support v1 and v2. But also, you can modify the server to handle v1 differently. E.g the absence of a “disable_new_feature” key automatically opts v1 callers into the new feature.
I think the details are are really coupled into what you are engineering and what you want to roll out. There’s no magic bullet, and this increases the complexity of your code. Good engineering in my opinion is deciding when these approaches are worth it and how they should be implemented.