rest api client cache

REST API: Improve performance with client side caching

[icegram campaigns="258"]

When developing REST API, there imminently comes a time when you need to think of performance. Usually, the first option that comes to mind is to use some kind of caching mechanism to improve performance.  Caching can dramatically improve performance of database operations, which are usually the biggest bottleneck in web applications.

However, in this post I will focus on a way we can leverage caching support built into HTTP on protocol level. Using these features can dramatically reduce server load and improve overall performance of our REST API. Sounds good? Keep reading and see what you can do to make your API fly!

Caching options in REST API

On high level, we can divide caching options for REST API into two categories: server side and client side caches.

  • Server side caching – these are mechanisms used on server, and they usually improve communication performance between components of your application, such as database servers, message queues, inter-service communication etc. For example, in previous post about caching in microservices, we’ve seen how we can use Redis and Memcached to improve microservices performance. These strategies are usually the first things considered for improving application performance
  • Client side caching – this is step further in performance improvement process. Using this mechanism, you can offload a lot of caching load from server to clients itself. If you have a lot of clients connecting to your API, you can achieve great improvements with this approach to caching.

By combining these two approaches, we can create robust and highly scalable REST API. For the purpose of this post, I’ve created a simple API based on Spring Boot which demonstrates concepts outlines here. You can get he complete API source code on Github.

Server side caching with Redis

Like I mentioned in previous section, sample application combines both server side and client side approaches to caching, in order to achieve best performance. This section will outline the approach I used for server side part of caching mechanism. It uses Redis as cache, and Spring’s built-in cache annotations to add caching support to application.

For the purpose of this example, let’s consider a service which returns a person data based on it’s ID. Person object contains first name and last name information. This is modeled as Person  class:

Service layer with cache support

In order to keep things simple, we won’t use any database operations or similar. Instead, we’ll simulate lengthy operations by putting thread to sleep for few seconds. By using this approach, we can focus on showing the difference in performance between uncached  API call and when cache kicks in.

Our PersonService  contains two methods, one to retrieve person based on ID, and the other to update person information. Bellow is the source code:

As you can see, getPerson  method simulates long operation. It is also annotated by @Cacheable  annotation, which will make the result cached by Redis. Person ID is used as cache key.

Method updatePerson  will invalidate the cache and update person information. We use @CacheEvict to invalidate cache based on person ID.

REST API endpoints

Final piece of API is to implement endpoints. Bellow is the source code of the controller:

Here, we define two endpoints:

  • GET /person{id} – return person based on ID
  • PUT /person  – update the person with data passed as request body

Running the sample

So far, we have functioning REST API with server side caching using Redis. To test the application, we will need Redis server, for which we will use Docker image. Assuming you have Docker installed on your machine, simply run the following command:

This will start Redis in background. Now, run the application, and invoke endpoint at http://localhost:8080/person/1 . The first time you call this endpoint, it will take some time to get the result. But, subsequent calls will return immediately, because result is now cached. Bellow is the sample screen shot:

 

rest api cache sample

Implement client side caching

OK, so far, we have server side caching working. But, this article focus should be on client side caching, so we’ll cover that part now.

HTTP specification defines several headers which are used to provide hints to clients how to cache resources. Browsers, as primary HTTP clients, have built in support for caching which is controlled by these headers. In this section, we’ll go through HTTP cache headers and see how they can be used in REST API.

‘Expires’ header

This header is the simplest way of telling the client how long the resource should be cached before it is considered invalid. This is simply a time stamp at which resource expires and looks something like this:

Note that time stamp format is very important and must be specified exactly as HTTP date (see HTTP RFC).

In modern browser, this header is mostly overriden by Cache-Control  header, but is still being used by some older clients. For compatibility, this header should be used in conjunction with Cache-Control , with it’s value matching one specified  in Cache-Control  header. Bellow is the code snippet showing how this header can be set in Spring Boot:

However, this header is not the best fit for use in REST API. By it’s very nature, APIs are dynamic and can change at any time. Therefore, unless you can tell exactly how long your resource will be valid, this header might not be the best choice. It is probably better suited for static content, such as images media etc.

One use case for Expires header might be if your API resources are updated at specific interval, and you know exactly when these updates happen (eg., when you receive message from external services in scheduled manner).

‘Cache-Control’ header

This header allows fine grained control over caching options for a resources. HTTP standard defines several directives which can you can use with this header. Following list contains most common ones:

  • public  – indicates that anyone can cache the response, including public proxy caches. Use this if response content is publicly viewable
  • private  – response is private and only single user can cache it. Use this option for sensitive data, such as user profiles and sensitive information
  • no-cache  – indicates that client must verify request with the server before releasing it to user. Client usually uses some form of validation, such as Etag  or Last-Modified  time
  • no-store  – unlike previous directive, indicates that browsers or any other cache can not store any version of resource. For each request, client requests fresh copy from server
  • max-age  – indicates how long (in seconds) resource can be considered fresh. Unlike Expires header, this one specifies duration relative to time of request
  • s-maxage  – similar to max-age, but only relevant to shared caches, like CDNs or public proxies

For the complete list of directives for Cache-Control, please see relevant page about Cache-Control header on MDN.

Fortunately, Spring framework provides builder class for Cache-Control  headers. Code snippet bellow shows how you can use this class to build a header:

In this snippet, we set header value to be private, max-age=3600 .

As you can see from this, Cache-Control  gives us more options to define caching policy for API resources. When you use it in REST API, you can define more precise control over storage and expiration of the resources. But still, we need better control over validation of resources, because we don’t want to a priori define when resources will expire. For that reason, we should use Etag  header to validate resources.

‘Etag’ header

Servers send Etag (entity tag) header with the response to indicate internal state of the resource. Value of Etag  can be arbitrary value, but usually is something like MD5 hash which uniquely identifies current state of resource. Clients will store this value along with the resource, and send it on subsequent requests to the server as the value of If-None-Match  header. For example, suppose that server sends the following headers with the response:

On each subsequent request for this resource, client will send the following header:

If resource did not change, server will send a response with status 304 ( Not Modified) , which indicates to client that it does not need to download resource again. Otherwise, server will send new content with 200 status code.

Spring framework ships with a filter which helps with handling of Etag  headers. However, this filter provides only shallow handling of this tag. What this means is that server will process the request in regular fashion, but will return the correct status code based on Etag value. This basically means that with this filter, you only save the bandwidth, but server load remains basically the same. You can read about this in more details in this Baeldung post on Etag handling with Spring.

In order to fully utilize Etag , we would need to have the server skip any processing of the request if resource did not change, and return 304 status immediately.  In the next section, we will try to modify our REST API to handle this correctly.

Deep handling of Etag in Spring

In order to fully utilize Etag  in our API, we need a way to represent a state of Person  resource. Because we want to keep things simple, we will use MD5 hash to produce Etag value for person. For this reason, we will create PersonDigestService  class, which will handle managing Etags for person resources. Code snippet bellow shows this class:

This class will supports creating, removing and getting Etag  value of resource. We keep the map of request URIs to Etag  values, which will allow us to check status of specific resources. To create Etag  value, we calculate MD5 hash of Person  object fields. This should be unique for each person.

Controller modification for Etag support

Now, we need to modify controller to calculate Etag when client requests the resource, and to remove it when they update it. Controller should look like this:

For GET  requests, we calculate Etag value based on person fields. When clients invokes PUT  endpoint to update person, we remove previous Etag  value.

Filter for Etag handling

Final piece of the puzzle is the filter that will be used to intercept incoming requests for person resource. Code snippet bellow shows filter code.

This filter works on simple principle: If client requests contains If-None-Match  header, we check if such value exists in digestService . If it does, filter returns immediately with 302 status, skipping any further processing on server. In case request does not contain If-None-Match  header, or it does not match existing Etag, processing continues as usual.

This way, we will save our server from processing incoming requests needlessly, if client already has the latest value.

Disclamer: Please note that this is just a proof of concept code, and should NOT be used in production environment.

Testing complete setup

We can now build and run our application to test if it works. If you invoke http://localhost:8080/person/1 , you should get response body, along with Etag  header, like in the image bellow:

rest api http etag header

In next request, we will set If-None-Match  header, and invoke the same endpoint. This time, we should get 302 HTTP status code, with no body. Check out the image bellow:

rest api if-none-match response

You may also notice that this time, request processing was faster, because the filter returns response immediately.

Conclusion

This was a long post, but it shows how you can improve your REST API performance with various caching techniques. In my opinion, Etag  headers can be a great tool for saving bandwidth and server load. In this example, we’ve seen only how to use Etag  for reading, but they can also be used for updating resource. In that case client send an If-Match  header which controls if resource can be updated or not. You can find an example here on how to do concurrent updates.

I would very much like to hear your opinion of this approach, so please feel free to voice your comments on this post.

 

Leave a Comment

Your email address will not be published. Required fields are marked *