Client libraries: a slippery slope across programming models.

Vincent Labrecque
Genetec Tech
Published in
6 min readMay 14, 2019

What could possibly be wrong about giving users an easier way to use a service you developed?

When developing services, we often want to provide our users with an extra artifact: a library. The underlying motivation is usually to give users something that fits their programming model more naturally than network calls.

As one example, detecting the failure modes of network calls and converting them into the natural failure manifestations for a given programming model, e.g. exceptions in many runtimes. Another typical use is to encode usage patterns, e.g. authenticating and then passing a token to a service. But as time goes, the service developers also owning a library can easily blur the boundary between client and server.

As people who know me probably expects, I’ll be looking at the gray area and not spending much time on the risks I consider obvious. This includes e.g. that client libraries is a platform dependency for users. These are “easily” understood, managed and mitigated: develop for an homogeneous set of platforms, use a “platform independent” language such as JavaScript, or what have you.

The items I will focus on are not necessarily things that will go wrong, but things that might more easily go wrong despite your best intentions. Places we all slip up. I’ll propose a few mitigation strategies, but specific situations need thinking on a case-by-case basis.

Setting the stage: why does this matter?

Do not conflate who writes the code with where it runs. A client library runs in the client. It doesn’t matter that it’s the service developer who writes it: that makes it client code.

Logic in this library is still logic running on the user side. If it helps, read this as “logic in the library is still logic in the UI.”

This should make the point clear: in a world with multiple clients and open APIs, it is important to keep all important bits behind the unchanging surface. That is to say: in the server.

Uses and risks.

An extreme, and correct, starting point would be a proxy library generated from an OpenAPI specification. This obviously cannot add anything on the wrong side of the client/server boundary. As we move from this correct library and add more code, what are the ways in which we can go wrong?

First is the risk to go for ease of use. We do want to make the service easier to use. This is often a good thing. Typical workflows can be encoded in the library, etc. What then is the risk? You now have two surfaces: the service itself, and the library. The risk is using a library to mask that the service isn’t fit for purpose. In most cases, fixing the service’s interface, or modeling an alternative workflow as another service, is a much better long-term strategy than having it reside in client code. This makes the service better for all, whether they’re trying it out with curl, scripting it with PowerShell or using your service from a mobile environment you hadn’t planned for. It also simplifies documenting the semantics of your service independently from the way to access it.

Second I would point out the risk of ease from the service developer’s perspective. There’s often many choices of where to implement a feature, or fix a bug. As developers we will usually go for the place that’s most comfortable to us. This makes the client library a very, very tempting place to go put any fix. It works and it is easier to deploy! No risk of downtime! No risk of the service falling over on its way back up! So what’s the risk? Despite the comforts listed just now, bug fixes and features rarely belong on the client side. Fixing some problems there is deeply wrong (e.g. security checks only on the client) and some lead to maintenance nightmare (e.g. having to duplicate validation rules in N client libraries).

A third risk is really intrinsic to developing components that interact. If you can change your service and library “at once”/”in a transaction”/”in a commit”, it is easy to forget that some users may not go through that useful library. An analogy here would be a risk associated to monorepos: one can do a consistent change to a repository that would break compatibility when components are deployed on their own. That is to say, the boundaries, the scope of consistency, are not the same between the source control and the deployment.

Mitigation.

My favorite meta-strategy for gray-area or slippery slope problems is to increase visibility. Assume competence and that if something goes wrong, it’s probably because no one saw it. For that reason, I prefer to focus on increasing likelihood of (early) detection.

Once again, let’s mention and then skip the obvious solution. Consider not providing a client library. In the many cases where that’s not possible or advisable, how can we detect whether something’s sneaked into the client library?

Below, I am assuming a code development process where one could track related changes. I will write this as “a pull request changing […]”, but depending on your repository layout and other factors, it may manifest in other ways. Whether it’s in the same transaction or whether it relies on the short term memory of developers on the team, these strategies should still increase the probability of detecting slip ups.

One way is to provide more than one such library. Maybe create a second one for another programming language. How does this help? In that case, a pull request changing 3 libraries at the same time is fairly easy to spot. You can then include this check in the code review checklist, etc. But it is also risky to have an extra library just for this reason, that isn’t seriously used. That library has a high risk of falling behind and then you may end up in a situation where a PR still only changes the frequently used library. And that leaves us in the same place we were before.

Another way is to insist on no code at all being shared between the service tests that the team maintains and the client library. Copy paste the code if you have to, but don’t share a library/assembly. This has a similar detection property as the previous strategy, but with a few additional advantages. One of them being that if the tests encode the usage patterns and don’t use the library, you know that (at least for those use cases) the service is doing the right thing. Another is similar to the one above: a PR containing changes to both the tests and the client library is an easy red flag to catch during review.

So what is it that belongs in a client library?

Another rule of thumb: the library shouldn’t contain concepts (verbs or nouns) that aren’t present in the service’s interface.

Things that are useful in libraries are also useful in documentation. For instance, the error handling/retry strategies, etc. So make sure you have written documentation first.

That usually makes for a very thin library focused on the initial issue, namely providing a mapping between the client application’s application model and the foreign service interface. As mentioned previously, an example of this would be using an automatically generated library from a protocol specification. This necessarily has limited value, and many bits of client side logic cannot be included with this. But it has the advantage of being often usable, and safe.

Conclusion.

As I wrote earlier, this is a gray area: what belongs on the server vs what belongs on the client. The key element to remember is heterogeneous network environments: many versions interact. That is the source of much pain. The more of the essential logic is held within the server’s scope, the easier this pain will be to understand, mitigate, and alleviate.

There is definitely value in some uses of client libraries, but one must be mindful of the traps. I have listed three traps, which I consider some of the trickier ones. Thinking from multiple viewpoints remains the best meta-strategy, when looking at a client library, don’t just look at it from the perspective of the initial integration. Balance out those benefits with the long-term systemic impact on yourself and other stakeholders by keeping it thin and simple.

Blind monks examining an elephant, by Hanabusa Itchō (1652–1724), via Wikipedia (Public domain)

Thanks to Romain Bertozzi, Olivier Matz and Joel Bourbonnais for their careful reading and comments on this article. Thanks to Milos Haravan for asking the question.

--

--