Specifically, I’m talking about the case of a back-end service that supports other back-end services within a single enterprise, e.g. in a microservice setup. To be fair, we can still get away with just exposing an HTTP endpoint or a queue where the client directly submits the message, but we can go a step further and make everyone’s life easier by providing a client library alongside the service.
With a well-designed client library, the service consumer gets:
- Less typing on the keyboard,
- Compile-time error checks instead of run-time checks,
- Easier mocking and unit testing,
- Hinting and autocomplete when using an IDE,
And in general all the good things that lead to reduced time to delivery on features and bug fixes.
These benefits are offset by the cost of writing and maintaining a client library, so it may not make sense to do so when the consumer should be allowed to use any language they want, e.g. in case of external-facing APIs. Or when there’s only one consumer for the client library, like a combination of tightly coupled back-end and front-end in a simple application.
Fortunately, there are often tools to make it easy to write and maintain client libraries.
Let’s suppose we’re writing a service that maintains the list of users. If the service consumer wants to add another user, they submit a POST request like this:
200 OK if everything went fine; otherwise, it returns a
5xx error code with a message in the body.
This will work fine and will do its job. The first consumer of the service will probably write their own copy of the model and will use a convert it to JSON with a simple one-liner that is available in their language of choice. They may add a wrapper for mocking the service in the unit tests as well. The second consumer will probably do the same but in a separate piece of code, different from the first one. And so will the third.
There is, of course, a chance that they will all get together and decide to do this in a central, shared library that will have message models, call signatures, and some convenience functionality like retries or logging. This assumes that the service consumers are all aware of each others’ actions, have the time and willingness to get together to do this, and that the company culture supports taking time out of writing features to do foundational work. From my observations, unless there’s planning in advance, the odds of this shared library emerging aren’t high.
In the above case, the advance planning would be writing the client library and making it available to the consumers of the service. This doesn’t have to be a lot of work. For the same user service, the library would include
AddUserMessage type, possibly a return type, and a piece of code that wraps the process of making the HTTP call and handling the response.
class AddUserMessage(val name: String, val email: String)
This is a basic blocking implementation and could use some improvement, but the service consumers already get: compile-time error checking, easy mocking for unit tests, and the IDE will help autocomplete the function and class names.
In cases of communicating over a queue, the client library would hide implementation details for the queue call, allowing the caller to think just about the call to the service and not worry about the underlying details. In case of async calls or long-running requests, the library could also provide correlation between the requests and responses, handling de-duplication, or managing other complexities. The client library could also manage retries, extracting meaningful error details, and emit progress notifications.
On a larger scale, there are some interesting examples of client libraries being generated automatically. For example, Michael Bryzek showed how Flow.io maintains a large number of microservices by generating client libraries from JSON declarations. The same generation system produces mock clients for testing; these mock clients have, according to Michael, mostly removed the need for integration testing.
In another example, AWS SDKs have all the signs of being automatically generated. Given Amazon’s other internal automation, it sounds unlikely that they chose instead to use an army of engineers to type out the highly repetitive implementation code.
Taking the idea to the next step, if the service consumer only uses the client library, then they do not need to be aware of the underlying protocol. Then we can switch from HTTP and REST to something like grpc and Protocol Buffers, and use the same declaration to generate multiple client libraries for several languages. At this point, unlike AWS above, we lose the option of supporting an arbitrary client language through an HTTP endpoint.
Taking this another step further, client libraries don’t even need to be just for services in the “microservice” sense. For example, an internal data warehouse could be considered a service. To improve cycle time for the whole company, the team maintaining the data warehouse could publish a shared library that provides type definitions and mappings (auto-generated from the database schema) that correspond to the underlying tables and views which it allows querying. Over time, the library could be enriched with change notification updates, transparent calls to cache instead of the database, query limits, and other restrictions that otherwise would have to be communicated to the developers.