1. Overview
In this tutorial, we’ll have a quick look at Infinispan, an in-memory key/value data store, and how we can utilise it in an embedded mode inside Quarkus applications.
Infinispan is an open-source in-memory database that can hold nearly any type of data, from plain-text to structured objects.
It offers many powerful features, including full-text search and vector search capabilities. These capabilities, when combined with the quarkus-embedded extension, allow us to create powerful microservices with an in-memory high-performance embedded cache.
2. Project Configuration
We’ll create an example project to help us test the capabilities of the Quarkus embedded extension.
2.1. Creating a Project
We require a JDK and Quarkus CLI installed as prerequisites. Once the prerequisites are available, we can create a new project:
quarkus create app --no-code quarkus-infinispan-example
This should create a new project under the folder named quarkus-infinispan-example with basic scaffolding for a Quarkus project.
2.2. Dependencies
The project created using the quarkus CLI automatically adds core dependencies (quarkus-arc and quarkus-junit5) and any additional dependencies based on the extensions we specify in the command:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
In addition, we need to add the quarkus-infinispan-embedded extension:
<dependency>
<groupId>io.quarkiverse.infinispan</groupId>
<artifactId>quarkus-infinispan-embedded</artifactId>
<version>1.1.0</version>
</dependency>
We note, here, that there is another similarly named core Quarkus extension called quarkus-infinispan-cache. It’s intended for the client-server mode of operation of Infinispan, not for embedded mode, and should therefore not be used on the classpath.
3. Infinispan Embedded Cache Setup
With the basic project and dependencies in place, we’re ready to start building. There are two ways to set up the cache – using a builder class called ConfigurationBuilder or using annotations.
3.1. Using Configuration Builder
This extension offers a convenient builder class with chainable methods to create custom configurations with fine-grained control.
We define a new @ApplicationScoped service class called InfinispanCacheService:
@ApplicationScoped
public class InfinispanCacheService {
public static final String CACHE_NAME = "demoCache";
@Inject
EmbeddedCacheManager cacheManager;
private Cache<String, String> demoCache;
@PostConstruct
void init() {
Configuration cacheConfig = new ConfigurationBuilder()
.clustering().cacheMode(CacheMode.LOCAL)
.memory().maxCount(10)
.expiration().lifespan(600, TimeUnit.MILLISECONDS)
.persistence().passivation(true).build();
demoCache = cacheManager.administration().withFlags(CacheContainerAdmin.AdminFlag.VOLATILE)
.getOrCreateCache(CACHE_NAME, cacheConfig);
}
}
In this example, we’ve injected the EmbeddedCacheManager, which helps interact with the Infinispan cache server. Then, the init method uses ConfigurationBuilder to create a local cache. This configuration is set up with a max memory of 10 objects and an expiration time of 600 ms. This cache also has passivation capabilities so that if the number of objects in memory exceeds 10, then the cache automatically persists the additional cached objects to local disk.
Finally, we use the EmbeddedCacheManager to create a cache with the name “demoCache”.
3.2. Using Annotations
If we don’t need the fine-grained control over the cache configuration, then a more convenient way is to use the @Embedded annotation available in the extension:
public static final String CACHE_NAME = "anotherCache";
@Embedded(CACHE_NAME)
@Inject
Cache<String, String> embeddedCache;
public Cache<String, String> getEmbeddedCache() {
return embeddedCache;
}
With this, we’ve used the @Embedded annotation from the quarkus-infinispan-embedded to apply the default configuration and set up an embedded Infinispan cache.
This extension applies sensible defaults for such caches. We can also override these defaults through the use of Infinispan’s XML configuration if needed. Such an XML config file can be configured in the application.properties using the quarkus.infinispan-embedded.xml-config property.
3.3. Cache Accessor Methods
The org.infinispan.Cache interface offers several methods similar to Java’s Map interface to enable basic operations such as getting and putting objects in the cache. We can use these methods to create wrapper methods around the cache inside the InfinispanCacheService class:
public void put(String key, String value) {
demoCache.put(key, value);
}
public String get(String key) {
return demoCache.get(key);
}
public void bulkPut(Map<String, String> entries) {
demoCache.putAll(entries);
}
public int size() {
return demoCache.size();
}
public void clear() {
demoCache.clear();
}
public void stop() {
cacheManager.stop();
}
Here, the service class doesn’t do anything special; however, these methods can include caching capabilities inside our services alongside doing other business logic inside the service class functions and enabling cache access for such business logic when needed.
3.4. Annotated Cache Accessor Methods
Quarkus also offers some built-in caching annotations, such as @CacheResult, @CacheInvalidate, and @CacheInvalidateAll, that allow us to run computations alongside or with accessing the cache. This extension supports these annotations out of the box.
For example, if we have a long-running computation that produces a result that doesn’t change often, then we can potentially cache that result using the @CacheResult annotation. This way, it’s cached after the first computation, and subsequent computations can simply return the result from the cache:
@CacheResult(cacheName = CACHE_NAME)
String getValueFromCache(String key) {
// simulate a long running computation
try {
System.out.println("getting value for "+ key);
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
return key + "Value";
}
That’s it. Our cache and its accessor methods are now defined, and we are ready to use the newly set up cache in our application.
Next, let’s run some tests with our cache.
4. Testing the Cache
So far, we’ve focused on the setup and configuration of the cache. It’s now time to check that everything works. We’ll use Junit for our tests.
4.1. Before Test
First, we need to ensure that each test starts with a clean setup:
@Inject
InfinispanCacheService cacheService;
@BeforeEach
void clearCache() {
cacheService.clear();
}
4.2. Basic Cache Test
Then, we run a test to confirm that objects are indeed added to the cache:
@Test
void givenNewCache_whenPutEntries_thenTheyAreStored() {
for (int i = 0; i < 10; i++) {
cacheService.put("key" + i, "value" + i);
}
assertEquals(10, cacheService.size());
assertEquals("value5", cacheService.get("key5"));
}
4.3. Expiration and Eviction Tests
We note that the cache also has a configuration with an expiration time of 600ms, a maximum of 10 objects in memory. So we run tests to confirm that this configuration works:
@Test
void givenEntryWithTTL_whenWaitForTTLToExpire_thenEntryIsExpired() throws InterruptedException {
cacheService.put("expireKey", "expireValue");
Thread.sleep(1000); // Wait past the 600-ms TTL
assertNull(cacheService.get("expireKey"));
}
@Test
void givenMaxEntryLimit_whenInsertMoreThanLimit_thenEvictionOccurs() {
Map<String, String> bulkEntries = new HashMap<>();
for (int i = 0; i < 200; i++) {
bulkEntries.put("evictKey" + i, "value" + i);
}
cacheService.bulkPut(bulkEntries);
assertTrue(cacheService.size() <= 10);
}
4.4. Annotated Method Tests
We’ve also set up methods that use the @Embedded annotation from Infinispan and the @CacheResult annotation from Quarkus. Let’s test them:
@Test
void givenCache_whenQuarkusAnnotatedMethodCalled_thenTheyAreStoredInCache() {
for (int i = 0; i < 10; i++) {
cacheService.getValueFromCache("storedKey" + i);
}
String embeddedValue9 = cacheService.getEmbeddedCache().get("storedKey9");
assertEquals("storedKey9Value",embeddedValue9);
}
With this, we’ve covered the key features of the quarkus-infinispan-embedded extension.
5. Conclusion
In this article, we configured an embedded Infinispan cache using Quarkus and the quarkus-infinispan-embedded extension. We tested some of the features provided by this extension, including compatibility with Quarkus caching annotations.
We note that Infinispan itself offers several other powerful features that are beyond the scope of this article.
The code backing this article is available on GitHub. Once you're
logged in as a Baeldung Pro Member, start learning and coding on the project.