Quarkus - Application Data Caching

In this guide, you will learn how to enable application data caching in any CDI managed bean of your Quarkus application.

This technology is considered preview.

In preview, backward compatibility and presence in the ecosystem is not guaranteed. Specific improvements might require to change configuration or APIs and plans to become stable are under way. Feedback is welcome on our mailing list or as issues in our GitHub issue tracker.

For a full list of possible extension statuses, check our FAQ entry.

Prerequisites

To complete this guide, you need:

  • less than 15 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.6.2+

Scenario

Let’s imagine you want to expose in your Quarkus application a REST API that allows users to retrieve the weather forecast for the next three days. The problem is that you have to rely on an external meteorological service which only accepts requests for one day at a time and takes forever to answer. Since the weather forecast is updated once every twelve hours, caching the service responses would definitely improve your API performances.

We’ll do that using a single Quarkus annotation.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

Clone the Git repository: git clone [https://github.com/quarkusio/quarkus-quickstarts.git](https://github.com/quarkusio/quarkus-quickstarts.git), or download an archive.

The solution is located in the cache-quickstart directory.

Creating the Maven project

First, we need to create a new Quarkus project using Maven with the following command:

  1. mvn io.quarkus:quarkus-maven-plugin:1.7.6.Final:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=cache-quickstart \
  4. -DclassName="org.acme.cache.WeatherForecastResource" \
  5. -Dpath="/weather" \
  6. -Dextensions="cache,resteasy-jsonb"

This command generates the Maven project with a REST endpoint and imports the cache and resteasy-jsonb extensions.

If you already have your Quarkus project configured, you can add the cache extension to your project by running the following command in your project base directory:

  1. ./mvnw quarkus:add-extension -Dextensions="cache"

This will add the following to your pom.xml:

  1. <dependency>
  2. <groupId>io.quarkus</groupId>
  3. <artifactId>quarkus-cache</artifactId>
  4. </dependency>

Creating the REST API

Let’s start by creating a service that will simulate an extremely slow call to the external meteorological service. Create src/main/java/org/acme/cache/WeatherForecastService.java with the following content:

  1. package org.acme.cache;
  2. import java.time.LocalDate;
  3. import javax.enterprise.context.ApplicationScoped;
  4. @ApplicationScoped
  5. public class WeatherForecastService {
  6. public String getDailyForecast(LocalDate date, String city) {
  7. try {
  8. Thread.sleep(2000L); (1)
  9. } catch (InterruptedException e) {
  10. Thread.currentThread().interrupt();
  11. }
  12. return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
  13. }
  14. private String getDailyResult(int dayOfMonthModuloFour) {
  15. switch (dayOfMonthModuloFour) {
  16. case 0:
  17. return "sunny";
  18. case 1:
  19. return "cloudy";
  20. case 2:
  21. return "chilly";
  22. case 3:
  23. return "rainy";
  24. default:
  25. throw new IllegalArgumentException();
  26. }
  27. }
  28. }
1This is where the slowness comes from.

We also need a class that will contain the response sent to the users when they ask for the next three days weather forecast. Create src/main/java/org/acme/cache/WeatherForecast.java this way:

  1. package org.acme.cache;
  2. import java.util.List;
  3. public class WeatherForecast {
  4. private List<String> dailyForecasts;
  5. private long executionTimeInMs;
  6. public WeatherForecast(List<String> dailyForecasts, long executionTimeInMs) {
  7. this.dailyForecasts = dailyForecasts;
  8. this.executionTimeInMs = executionTimeInMs;
  9. }
  10. public List<String> getDailyForecasts() {
  11. return dailyForecasts;
  12. }
  13. public long getExecutionTimeInMs() {
  14. return executionTimeInMs;
  15. }
  16. }

Now, we just need to update the generated WeatherForecastResource class to use the service and response:

  1. package org.acme.cache;
  2. import java.time.LocalDate;
  3. import java.util.Arrays;
  4. import java.util.List;
  5. import javax.inject.Inject;
  6. import javax.ws.rs.GET;
  7. import javax.ws.rs.Path;
  8. import javax.ws.rs.Produces;
  9. import javax.ws.rs.core.MediaType;
  10. import org.jboss.resteasy.annotations.jaxrs.QueryParam;
  11. @Path("/weather")
  12. public class WeatherForecastResource {
  13. @Inject
  14. WeatherForecastService service;
  15. @GET
  16. @Produces(MediaType.APPLICATION_JSON)
  17. public WeatherForecast getForecast(@QueryParam String city, @QueryParam long daysInFuture) { (1)
  18. long executionStart = System.currentTimeMillis();
  19. List<String> dailyForecasts = Arrays.asList(
  20. service.getDailyForecast(LocalDate.now().plusDays(daysInFuture), city),
  21. service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 1L), city),
  22. service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 2L), city)
  23. );
  24. long executionEnd = System.currentTimeMillis();
  25. return new WeatherForecast(dailyForecasts, executionEnd - executionStart);
  26. }
  27. }
1If the daysInFuture query parameter is omitted, the three days weather forecast will start from the current day. Otherwise, it will start from the current day plus the daysInFuture value.

We’re all done! Let’s check if everything’s working.

First, run the application using ./mvnw compile quarkus:dev from the project directory.

Then, call [http://localhost:8080/weather?city=Raleigh](http://localhost:8080/weather?city=Raleigh) from a browser. After six long seconds, the application will answer something like this:

  1. {"dailyForecasts":["MONDAY will be cloudy in Raleigh","TUESDAY will be chilly in Raleigh","WEDNESDAY will be rainy in Raleigh"],"executionTimeInMs":6001}

The response content may vary depending on the day you run the code.

You can try calling the same URL again and again, it will always take six seconds to answer.

Enabling the cache

Now that your Quarkus application is up and running, let’s tremendously improve its response time by caching the external meteorological service responses. Update the WeatherForecastService class like this:

  1. package org.acme.cache;
  2. import java.time.LocalDate;
  3. import javax.enterprise.context.ApplicationScoped;
  4. import io.quarkus.cache.CacheResult;
  5. @ApplicationScoped
  6. public class WeatherForecastService {
  7. @CacheResult(cacheName = "weather-cache") (1)
  8. public String getDailyForecast(LocalDate date, String city) {
  9. try {
  10. Thread.sleep(2000L);
  11. } catch (InterruptedException e) {
  12. Thread.currentThread().interrupt();
  13. }
  14. return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
  15. }
  16. private String getDailyResult(int dayOfMonthModuloFour) {
  17. switch (dayOfMonthModuloFour) {
  18. case 0:
  19. return "sunny";
  20. case 1:
  21. return "cloudy";
  22. case 2:
  23. return "chilly";
  24. case 3:
  25. return "rainy";
  26. default:
  27. throw new IllegalArgumentException();
  28. }
  29. }
  30. }
1We only added this annotation (and the associated import of course).

Let’s try to call [http://localhost:8080/weather?city=Raleigh](http://localhost:8080/weather?city=Raleigh) again. You’re still waiting a long time before receiving an answer. This is normal since the server just restarted and the cache was empty.

Wait a second! The server restarted by itself after the WeatherForecastService update? Yes, this is one of Quarkus amazing features for developers called live coding.

Now that the cache was loaded during the previous call, try calling the same URL. This time, you should get a super fast answer with an executionTimeInMs value close to 0.

Let’s see what happens if we start from one day in the future using the [http://localhost:8080/weather?city=Raleigh&daysInFuture=1](http://localhost:8080/weather?city=Raleigh&daysInFuture=1) URL. You should get an answer two seconds later since two of the requested days were already loaded in the cache.

You can also try calling the same URL with a different city and see the cache in action again. The first call will take six seconds and the following ones will be answered immediately.

Congratulations! You just added application data caching to your Quarkus application with a single line of code!

Do you want to learn more about the Quarkus application data caching abilities? The following sections will show you everything there is to know about it.

Caching annotations

Quarkus offers a set of annotations that can be used in a CDI managed bean to enable caching abilities.

@CacheResult

Loads a method result from the cache without executing the method body whenever possible.

When a method annotated with @CacheResult is invoked, Quarkus will compute a cache key and use it to check in the cache whether the method has been already invoked. If the method has one or more arguments, the key computation is done from all the method arguments if none of them is annotated with @CacheKey, or all the arguments annotated with @CacheKey otherwise. Each non-primitive method argument that is part of the key must implement equals() and hashCode() correctly for the cache to work as expected. This annotation can also be used on a method with no arguments, a default key derived from the cache name is generated in that case. If a value is found in the cache, it is returned and the annotated method is never actually executed. If no value is found, the annotated method is invoked and the returned value is stored in the cache using the computed or generated key.

A method annotated with CacheResult is protected by a lock on cache miss mechanism. If several concurrent invocations try to retrieve a cache value from the same missing key, the method will only be invoked once. The first concurrent invocation will trigger the method invocation while the subsequent concurrent invocations will wait for the end of the method invocation to get the cached result. The lockTimeout parameter can be used to interrupt the lock after a given delay. The lock timeout is disabled by default, meaning the lock is never interrupted. See the parameter Javadoc for more details.

This annotation cannot be used on a method returning void.

Quarkus is able to also cache null values unlike the underlying Caffeine provider. See more on this topic below.

@CacheInvalidate

Removes an entry from the cache.

When a method annotated with @CacheInvalidate is invoked, Quarkus will compute a cache key and use it to try to remove an existing entry from the cache. If the method has one or more arguments, the key computation is done from all the method arguments if none of them is annotated with @CacheKey, or all the arguments annotated with @CacheKey otherwise. This annotation can also be used on a method with no arguments, a default key derived from the cache name is generated in that case. If the key does not identify any cache entry, nothing will happen.

If the @CacheResult or @CacheInvalidate annotations are used on a method with no parameters, a unique default cache key derived from the cache name will be generated and used.

@CacheInvalidateAll

When a method annotated with @CacheInvalidateAll is invoked, Quarkus will remove all entries from the cache.

@CacheKey

When a method argument is annotated with @CacheKey, it is identified as a part of the cache key during an invocation of a method annotated with @CacheResult or @CacheInvalidate.

This annotation is optional and should only be used when some of the method arguments are NOT part of the cache key.

Composite cache key building logic

When a cache key is built from several method arguments, whether they are explicitly identified with @CacheKey or not, the building logic depends on the order of these arguments in the method signature. On the other hand, the arguments names are not used at all and do not have any effect on the cache key.

  1. package org.acme.cache;
  2. import javax.enterprise.context.ApplicationScoped;
  3. import io.quarkus.cache.CacheInvalidate;
  4. import io.quarkus.cache.CacheResult;
  5. @ApplicationScoped
  6. public class CachedService {
  7. @CacheResult(cacheName = "foo")
  8. public Object load(String keyElement1, Integer keyElement2) {
  9. // Call expensive service here.
  10. }
  11. @CacheInvalidate(cacheName = "foo")
  12. public void invalidate1(String keyElement2, Integer keyElement1) { (1)
  13. }
  14. @CacheInvalidate(cacheName = "foo")
  15. public void invalidate2(Integer keyElement2, String keyElement1) { (2)
  16. }
  17. @CacheInvalidate(cacheName = "foo")
  18. public void invalidate3(Object notPartOfTheKey, @CacheKey String keyElement1, @CacheKey Integer keyElement2) { (3)
  19. }
  20. @CacheInvalidate(cacheName = "foo")
  21. public void invalidate4(Object notPartOfTheKey, @CacheKey Integer keyElement2, @CacheKey String keyElement1) { (4)
  22. }
  23. }
1Calling this method WILL invalidate values cached by the load method even if the key elements names have been swapped.
2Calling this method WILL NOT invalidate values cached by the load method because the key elements order is different.
3Calling this method WILL invalidate values cached by the load method because the key elements order is the same.
4Calling this method WILL NOT invalidate values cached by the load method because the key elements order is different.

Configuring the underlying caching provider

This extension uses Caffeine as its underlying caching provider. Caffeine is a high performance, near optimal caching library.

Caffeine configuration properties

Each of the Caffeine caches backing up the Quarkus application data caching extension can be configured using the following properties in the application.properties file. By default caches do not perform any type of eviction if not configured.

You need to replace cache-name in all of the following properties with the real name of the cache you want to configure.

About the Duration format

The format for durations uses the standard java.time.Duration format. You can learn more about it in the Duration#parse() javadoc.

You can also provide duration values starting with a number. In this case, if the value consists only of a number, the converter treats the value as seconds. Otherwise, PT is implicitly prepended to the value to obtain a standard java.time.Duration format.

Here’s what your cache configuration could look like:

  1. quarkus.cache.caffeine."foo".initial-capacity=10 (1)
  2. quarkus.cache.caffeine."foo".maximum-size=20
  3. quarkus.cache.caffeine."foo".expire-after-write=60S
  4. quarkus.cache.caffeine."bar".maximum-size=1000 (2)
1The foo cache is being configured.
2The bar cache is being configured.

Context propagation

This extension relies on non-blocking calls internally for cache values computations. By default, there’s no context propagation between the calling thread (from your application) and a thread that performs such a computation.

The context propagation can be enabled for this extension by simply adding the quarkus-smallrye-context-propagation extension to your project.

If you see a javax.enterprise.context.ContextNotActiveException in your application log during a cache computation, then you probably need to enable the context propagation.

You can find more information about context propagation in Quarkus in the dedicated guide.

Annotated beans examples

Implicit simple cache key

  1. package org.acme.cache;
  2. import javax.enterprise.context.ApplicationScoped;
  3. import io.quarkus.cache.CacheInvalidate;
  4. import io.quarkus.cache.CacheInvalidateAll;
  5. import io.quarkus.cache.CacheResult;
  6. @ApplicationScoped
  7. public class CachedService {
  8. @CacheResult(cacheName = "foo")
  9. public Object load(Object key) { (1)
  10. // Call expensive service here.
  11. }
  12. @CacheInvalidate(cacheName = "foo")
  13. public void invalidate(Object key) { (1)
  14. }
  15. @CacheInvalidateAll(cacheName = "foo")
  16. public void invalidateAll() {
  17. }
  18. }
1The cache key is implicit since there’s no @CacheKey annotation.

Explicit composite cache key

  1. package org.acme.cache;
  2. import javax.enterprise.context.Dependent;
  3. import io.quarkus.cache.CacheInvalidate;
  4. import io.quarkus.cache.CacheInvalidateAll;
  5. import io.quarkus.cache.CacheKey;
  6. import io.quarkus.cache.CacheResult;
  7. @Dependent
  8. public class CachedService {
  9. @CacheResult(cacheName = "foo")
  10. public String load(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
  11. // Call expensive service here.
  12. }
  13. @CacheInvalidate(cacheName = "foo")
  14. public void invalidate(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
  15. }
  16. @CacheInvalidateAll(cacheName = "foo")
  17. public void invalidateAll() {
  18. }
  19. }
1The cache key is explicitly composed of two elements. The method signature also contains a third argument which is not part of the key.

Default cache key

  1. package org.acme.cache;
  2. import javax.enterprise.context.Dependent;
  3. import io.quarkus.cache.CacheInvalidate;
  4. import io.quarkus.cache.CacheInvalidateAll;
  5. import io.quarkus.cache.CacheResult;
  6. @Dependent
  7. public class CachedService {
  8. @CacheResult(cacheName = "foo")
  9. public String load() { (1)
  10. // Call expensive service here.
  11. }
  12. @CacheInvalidate(cacheName = "foo")
  13. public void invalidate() { (1)
  14. }
  15. @CacheInvalidateAll(cacheName = "foo")
  16. public void invalidateAll() {
  17. }
  18. }
1A unique default cache key derived from the cache name is generated and used.

Multiple annotations on a single method

  1. package org.acme.cache;
  2. import javax.inject.Singleton;
  3. import io.quarkus.cache.CacheInvalidate;
  4. import io.quarkus.cache.CacheInvalidateAll;
  5. import io.quarkus.cache.CacheResult;
  6. @Singleton
  7. public class CachedService {
  8. @CacheInvalidate(cacheName = "foo")
  9. @CacheResult(cacheName = "foo")
  10. public String forceCacheEntryRefresh(Object key) { (1)
  11. // Call expensive service here.
  12. }
  13. @CacheInvalidateAll(cacheName = "foo")
  14. @CacheInvalidateAll(cacheName = "bar")
  15. public void multipleInvalidateAll(Object key) { (2)
  16. }
  17. }
1This method can be used to force a refresh of the cache entry corresponding to the given key.
2This method will invalidate all entries from the foo and bar caches with a single call.

Negative caching and nulls

Sometimes one wants to cache the results of an (expensive) remote call. If the remote call fails, one may not want to cache the result or exception, but rather re-try the remote call on the next invocation.

A simple approach could be to catch the exception and return null, so that the caller can act accordingly:

Sample code

  1. public void caller(int val) {
  2. Integer result = callRemote(val); (1)
  3. if (result == null) {
  4. System.out.println("Result is " + result);
  5. else {
  6. System.out.println("Got an exception");
  7. }
  8. }
  9. @CacheResult(name = "foo")
  10. private Integer callRemote(int val) {
  11. try {
  12. Integer val = remoteWebServer.getResult(val); (2)
  13. return val;
  14. } catch (Exception e) {
  15. return null; (3)
  16. }
  17. }
1Call the method to call the remote
2Do the remote call and return its result
3Return in case of exception

This approach has an unfortunate side effect: as we said before, Quarkus can also cache null values. Which means that the next call to callRemote() with the same parameter value will be answered out of the cache, returning null and no remote call will be done. This may be desired in some scenarios, but usually one wants to retry the remote call until it returns a result.

Let exceptions bubble up

To prevent the cache from caching (marker) results from a remote call, we need to let the exception bubble out of the called method and catch it at the caller side:

With Exception bubbling up

  1. public void caller(int val) {
  2. try {
  3. Integer result = callRemote(val); (1)
  4. System.out.println("Result is " + result);
  5. } catch (Exception e) {
  6. System.out.println("Got an exception");
  7. }
  8. @CacheResult(name = "foo")
  9. private Integer callRemote(int val) throws Exception { (2)
  10. Integer val = remoteWebServer.getResult(val); (3)
  11. return val;
  12. }
1Call the method to call the remote
2Exceptions may bubble up
3This can throw all kinds of remote exceptions

When the call to the remote throws an exception, the cache does not store the result, so that a subsequent call to callRemote() with the same parameter value will not be answered out of the cache. It will instead result in another attempt to call the remote.

The previous code example has the side-effect that the cache logs the thrown exception with a long stack trace to the console. This is done to inform developers that something exceptional has happened, but if you have a setup like above, you are already catching the exception and know what you are doing.