Skip to content
Logo Theodo

How to add cache in your web application

Quentin Scabello7 min read

Pull request with major changes

In my recent project, I added a cool new feature: if our third-party provider didn’t have your company address, you can pop it in yourself ! But guess what ? When I tried it out in a test environment, it didn’t play ball, even though it worked like a charm on my local setup. The culprit ? A sneaky cache issue when calling the third-party provider. Dive into this article, and I’ll share what I uncovered and learned !

First, what is a cache?

A cache is a hardware or software component used to temporarily store data in a computing environment. Caches were created to improve processor performance by allowing them to access data without retrieving it from central memory.

In processor, there are differents cache levels allowing the CPU to not fetch the required data in the system memory but to fetch it in the caches

Browser Caching

Let’s talk about browser cache. Every browser has a cache. The way it works is quite simple: when your browser requests a resource from a server, it checks its cache for that resource. If it finds it, it will not request the server; if not, it will call the server. When the server responds with the resource, the browser will cache the data.

Chrome reads in the HTML that it needs to donwload /image/3, first chrome check its cache for this image. The cache is empty so chrome do download the image thought a network get request

Cache-Control

The browser will use the “cache-control” header from the response to cache the requested resource. This header contains directives for browser caching. To simply understand caching, a resource is considered fresh when it’s still valid and can be reused. The resource is considered stale when it has been in the cache for too long and has expired. It cannot be reused.

The cache control value of a HTTP request can be found in the devTools. When clicking on a request in the network section, you can find the header in the "Response headers" part

Here are some examples:

You can find all the directives for cache control on the mcn documentation.

Validating a stale resource

To validate a resource, your browser can request the origin server using a header like last-modified or Etag. The Last-Modified header contains a date indicating the last time the resource was modified. If this date is available, your browser will send a request to the origin server with an If-Modified-Since header. If the resource has not been modified since that date, the server will respond with a 304 Not Modified status code, indicating that the cached version can be used. Otherwise, the server will provide the updated resource.

An example of a 304 request to validate a cache entry

The Etag header is a unique identifier for a resource that is generated by the server. If the resource changes, the Etag changes as well. To validate a resource using its Etag, send a request to the origin server with the header “If-None-Match” followed by the Etag. The server will respond with a 304 status code or the updated resource. The advantage of checking a resource is that a 304 response from the server is much smaller than a full resource. There is no body and nothing to parse or download

You can easily find cache information in the DevTools by checking if a request uses cached resources (indicated by a 304 status code), or by directly viewing the cached data in the Application tab.

Etag and Last-Modified in devTools

Caching in a Java application

Let’s talk about caching in web applications. Caching is a broad concept with many use cases, such as:

To resolve my problem, I had to delve deep into the third option. For Java web applications, there are several well-known caching solutions available, including Memcached, Varnish, Ignite, and Redis. I used Redis, which was already used in my project.

Redis

Redis logo Redis (for Remote Dictionary Server) is an open-source key/value database. Redis is known to be fast, having a large variety of stock types and having a possibility of data persistence.

There are two main libraries for Redis in Java: Lettuce and Jedis.

To establish a connection between the server and the Redis instance, we will use Lettuce. For caching purposes, Jedis will be used in the code.

To enable caching behaviour, we need to create a configuration file with two annotations: @EnableCaching and @Configuration. @Configuration indicates that the class has one or more @Bean definition methods. @EnableCaching triggers a process where every Bean is scanned for a potential public method with @Cache. If some methods are found, a proxy is created to intercept the method calls and handle the caching behaviour.

Cached method calls are intercepted by a component named RedisCacheManager, which calls the RedisTemplate. This last component is essential because developers can use it to perform specific operations in the cache, such as reading, writing, or testing the existence of a value. The RedisTemplate then uses the connection to call the Redis instance.

However, before that, the RedisTemplate needs to serialize the cached object since Redis doesn’t understand Java objects. That’s where the RedisSerializer comes into play. It serializes or deserializes objects to be used by Redis or the application. This completes the Redis work chain. Redis chain

Good practices when caching

Finally, we will discuss some best practices for using Redis.

Pay attention to your key

As previously stated, Redis is a key-value database. When caching the result of a cacheable method, Redis uses the method arguments to create the key. If your method only has zero, only one entry will be in your cache. Of your method has one simple argument (such as a number or a string), the key will be simple. Redis key for method with one argument However, if your method uses more arguments or more complex arguments, such as lists or custom objects, you need to be careful. Redis key for method with multiple complex arguments As you can see, Redis will build a key using all the arguments. If you want to retrieve this value, you will need to build the string representing all your arguments. That’s very difficult. To resolve this problem, you can build a custom key by using the parameter KeyGenerator from the annotation @Cacheable. You just need to implement an interface composed of one method.

Pay attention to the freshness of your data

It is important to ensure the freshness of your data in the cache. For instance, consider caching a method that returns user information with an address. If a user changes their address, you need to catch this change and either modify your cached data or delete the corresponding line in your cache. You can modify or delete a value in your cache using either of two methods: Use the RedisTemplate. It offers many useful functions for performing CRUD operations in your cache. Use annotations such as @CachePut or @CacheEvict. Use @CachePut on the method that modifies your cached data to update the modified data in the cache.

public class UserService {
    ...

  @CachePut(value = "user", key = "#userId")
  public User patchUserInfo(User user, Long userId) {
    return to(userRepository.save(to(user, userId)));
  }
}

Use @CacheEvict to delete the value in your cache.

public interface UserRepository extends JpaRepository<UserEntity, UUID> {

  @CacheEvict(cacheNames = "user", key = "#userEntity.userId")
  @Override
  UserEntity save(UserEntity userEntity);
}

Conclusion

By using the cache-control header, browsers are able to cache resources such as images, scripts, or HTTP responses. In Java, Redis can be used to easily cache data throughout your application.

Caching can greatly enhance performance by minimizing redundant HTTP or method calls. However, it is important to exercise caution when implementing caching, as a small mistake can result in troublesome bugs. It is not a miraculous solution that resolves all performance issues.

Liked this article?