The idea is to clearly separate the interface used by end-developers (consumers) from the inner workings of the API client, dealing with serialization and other IO gritty details.
There are many API clients out there and framework providing some quick-and-dirty way to code clients.
Most (of those I experienced) don't provide a lot of inner-working customizations.
Most (if not all) however do provide some basic abilities like number of attempts and some level of timeouts.
I'm looking here at a deeper and more intricate level to show how modular an API Client can should be and the benefits such architecture can bring.
Deep Dive
All an end-developer sees, and should see, from an API Client is simply methods with a parameter and a response, provided by the API interface.
![]() |
| Fig. 1. API Method |
... public AllocateAddressResult allocateAddress(); public AllocateAddressResult allocateAddress(AllocateAddressRequest request); public CreateImageResult createImage(CreateImageRequest request); public void deleteVolume(DeleteVolumeRequest request); ...
I would argue that all methods of an API Client should return something. Even if it's just an acknowledgment, it's more efficient to return an error message when the action could not occur on the server side than throwing an exception by design.
There's nothing extraordinary about all this because it's simple. Yet that simplicity allows for more elaborate inner workings without incurring further complications in its usage.
API Method
The goal of an API Client is to provide the means to easily communicate with a remote service.
This communication is specific to the implementation(s) of that service and the protocol(s) supported by that service: HTTP, FTP, SMTP, SPDY, etc.
This means that one needs to go from the model defined by the API to the model defined by the communication layer with the service and back: this involves marshaling, communicating, and un-marshaling, as illustrated below:
![]() | |
|
The Method from Fig. 1. is composed of the following:
- Marshaller: this piece of code is responsible to translate the API model object (Request) to a format the Network I/O knows how to transmit. For example, an Apache HTTPRequest implementation, a Netty ByteBuf, etc.
- Network I/O (NIO): simply delivers a formatted request over the network and wait for a response. This could be an Apache HttpClient, a Square OkHttpClient, etc.
- Unmarshaller: once a response from the service has been received, this piece of code will translate it into an API model object (Response here). For example, this code would translate an Apache HTTPResponse into an AllocateAddressResult object from AWS EC2 API.
Although a simple and obvious design, a lot can be said about it. There are 3 moving parts and anything can happen in any of those parts.
- Marshaller and unmarshaller could raise exceptions in case of schema mismatch (versions) for example.
- The NIO can (will) throw exceptions in case of network failures, timeouts, etc.
Interior Decorators
In previous posts about workers and commands, I introduced some useful patterns like the Retry Pattern, the Circuit Breaker Pattern and the Supervisor Pattern. Those can be used within the implementation of a client method to provide more anti-fragile features to the whole client.
The behavior and expectations of each of those three components mentioned above are very different: a Marshaller/Unmarshaller would not likely need a supervisor to make sure they complete under a certain amount of time. A Circuit breaker would make more sense at the NIO level to short-circuit it in case of problems than around serialization components. It would also make sense to retry certain NIO by plugging in a retry component.
![]() |
| Fig 3. Decorated |
Fig. 3 shows how one could improve the implementation of an API client method to provide more benefits without affecting the core functionality.
Once again, from a software client perspective, we're still calling a method passing a Request entity and expecting a result with the Response entity.
But now, without changing any calls, we (as a client) can expect that our application won't hang (thanks to supervisor). If we added a retry element, we could also expect a client method that is more network resilient. And as an API provider, we limit flooding our infrastructure with requests when it goes down or unresponsive, thanks to the circuit breaker.
Unmarshaller
A good practice is to implement a Tolerant Reader[1] where the code only reads what it needs and simply ignore anything it doesn't.
The advantage is to be more resilient to message schema changes. A more finicky reader might make life much harder when rolling out new versions of the API.
A Tolerant Reader makes a client and its service more forgiving.
Resources
[1] Tolerant Reader, Martin Fowler, 05/01/2011
[2] Schematron, Ian Robinson, 06/12/2006
[3] Thoughts on API Clients, Luke Pezet, 05/28/2014

.png)


No comments:
Post a Comment