How to connect your Quarkus application to Infinispan

Learn how to connect a Quarkus 3.x application to an Infinispan server using the Hot Rod client protocol, RESTEasy Reactive, and Jakarta EE standards. This guide covers ProtoStream schema generation for object marshalling, CRUD REST operations, and embedding Infinispan cache instances directly in Quarkus.

Infinispan is a distributed in-memory key/value data grid. An in-memory data grid is a form of middleware that stores sets of data for use in one or more applications, primarily in memory. There are different clients available to connect to a remote or embedded Infinispan server. In this tutorial, we will learn how to connect to Infinispan using the updated Quarkus 3.x extension and Jakarta EE specifications.

Starting Infinispan

For the purpose of this tutorial, we will be running a local Infinispan server with the following cache definition in infinispan.xml:

<cache-container default-cache="local">
      <transport cluster="${infinispan.cluster.name}" stack="${infinispan.cluster.stack:tcp}" node-name="${infinispan.node.name:}"/>
      <local-cache name="local"/>
      <invalidation-cache name="invalidation" mode="SYNC"/>
      <replicated-cache name="repl-sync" mode="SYNC"/>
      <distributed-cache name="dist-sync" mode="SYNC"/>
   </cache-container>

From the bin folder of Infinispan, run:

./server.sh

As an alternative, you can also run Infinispan using Docker as follows:

docker run -it -p 11222:11222 -e USER="admin" -e PASS="password" infinispan/server:latest

Creating the Quarkus Infinispan project

In order to create the Quarkus 3.x application, we will need the following set of dependencies in our pom.xml file, leveraging RESTEasy Reactive and the Hot Rod Infinispan client extension:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-infinispan-client</artifactId>
</dependency>

RESTEasy Reactive with Jackson handles high-performance HTTP endpoints, while the quarkus-infinispan-client dependency connects Quarkus to the remote Infinispan grid via Hot Rod protocol.

Here is our main Application listener class using Jakarta EE annotations:

package com.mastertheboss.infinispan;

import io.quarkus.infinispan.client.Remote;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.*;
import org.infinispan.client.hotrod.event.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ApplicationScoped
public class InfinispanClientApp {

    private static final Logger LOGGER = LoggerFactory.getLogger("InfinispanClientApp");

    @Inject
    @Remote("local")
    RemoteCache<String, Customer> cache;

    void onStart(@Observes StartupEvent ev) {
        cache.addClientListener(new EventPrintListener());
        Customer c = new Customer("1", "John", "Smith");
        cache.put("1", c);
    }

    @ClientListener
    static class EventPrintListener {

        @ClientCacheEntryCreated
        public void handleCreatedEvent(ClientCacheEntryCreatedEvent e) {
            LOGGER.info("Someone has created an entry: " + e);
        }

        @ClientCacheEntryModified
        public void handleModifiedEvent(ClientCacheEntryModifiedEvent e) {
            LOGGER.info("Someone has modified an entry: " + e);
        }

        @ClientCacheEntryRemoved
        public void handleRemovedEvent(ClientCacheEntryRemovedEvent e) {
            LOGGER.info("Someone has removed an entry: " + e);
        }
    }
}

When the application bootstraps, we connect to the RemoteCache ("local"), attach a Hot Rod ClientListener to it, and insert an initial entry.

To marshal and unmarshal custom Java objects like Customer across Infinispan endpoints, we utilize ProtoStream serialization with @ProtoField and @ProtoFactory annotations:

package com.mastertheboss.infinispan;

import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;
import java.util.Objects;

public class Customer {
    private String id;
    private String name;
    private String surname;
    
    @ProtoField(number = 1)
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
    
    @ProtoFactory
    public Customer(String id, String name, String surname) {
        this.id = id;
        this.name = name;
        this.surname = surname;
    }

    @ProtoField(number = 2)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ProtoField(number = 3)
    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", surname='" + surname + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Customer)) return false;
        Customer customer = (Customer) o;
        return Objects.equals(id, customer.id) &&
                Objects.equals(name, customer.name) &&
                Objects.equals(surname, customer.surname);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, surname);
    }

    public Customer() {
    }
}

Next, we declare a SerializationContextInitializer interface decorated with @AutoProtoSchemaBuilder to generate the Protocol Buffer schemas automatically during compile time:

package com.mastertheboss.infinispan;

import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;

@AutoProtoSchemaBuilder(includeClasses = { Customer.class }, schemaPackageName = "customer_list")
public interface CustomerContextInitializer extends SerializationContextInitializer {
}

Now, we create a reactive REST Endpoint using Jakarta REST annotations to execute CRUD operations against our remote cache:

package com.mastertheboss.infinispan;

import io.quarkus.infinispan.client.Remote;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.infinispan.client.hotrod.RemoteCache;

@Path("/infinispan")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class InfinispanEndpoint {

    @Inject
    @Remote("local")
    RemoteCache<String, Customer> cache;

    @GET
    @Path("/{customerId}")
    public Response get(@PathParam("customerId") String id) {
        Customer customer = cache.get(id);
        System.out.println("Got customer " + customer);
        if (customer == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        return Response.ok(customer).build();
    }

    @POST
    public Response create(Customer customer) {
        cache.put(customer.getId(), customer);
        System.out.println("Created customer " + customer);
        return Response.status(Response.Status.CREATED).entity(customer).build();
    }

    @PUT
    public Response update(Customer customer) {
        cache.put(customer.getId(), customer);
        System.out.println("Updated customer " + customer);
        return Response.status(Response.Status.ACCEPTED).entity(customer).build();
    }

    @DELETE
    @Path("/{customerId}")
    public Response delete(@PathParam("customerId") String id) {
        cache.remove(id);
        System.out.println("Deleted customer " + id);
        return Response.noContent().build();
    }
}

Finally, configure the host connection details and credentials inside src/main/resources/application.properties:

quarkus.infinispan-client.hosts=localhost:11222
quarkus.infinispan-client.username=admin
quarkus.infinispan-client.password=password

Running the Quarkus application

Start your application using the Quarkus Dev mode:

mvn clean quarkus:dev

Test the endpoints via curl. To retrieve the customer initialized at startup with ID "1":

curl http://localhost:8080/infinispan/1
{"id":"1","name":"John","surname":"Smith"}

To create a new Customer:

curl -d '{"id":"2", "name":"Clark","surname":"Kent"}' -H "Content-Type: application/json" -X POST http://localhost:8080/infinispan

To update an existing Customer:

curl -d '{"id":"2", "name":"Peter","surname":"Parker"}' -H "Content-Type: application/json" -X PUT http://localhost:8080/infinispan

And to remove a Customer from the cluster:

curl -X DELETE http://localhost:8080/infinispan/2

Source code for this tutorial: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/infinispan-demo

Embedding Infinispan in Quarkus applications

It is also possible to bootstrap an Infinispan cache container inside a Quarkus application using the embedded API. To do so, add the following extension to your pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-infinispan-embedded</artifactId>
</dependency>

With this dependency added, inject the org.infinispan.manager.EmbeddedCacheManager directly into your Jakarta EE beans:

@Inject
EmbeddedCacheManager emc;

This provides a local embedded cache container. If you wish to build a multi-node cluster programmatically, configure it via Java API or an external XML configuration file like dist.xml:

ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.clustering().cacheMode(CacheMode.DIST_SYNC);

List<EmbeddedCacheManager> managers = new ArrayList<>(3);
try {
    System.setProperty("jgroups.tcp.address", "127.0.0.1");
    for (int i = 0; i < 3; i++) {
        EmbeddedCacheManager ecm = new DefaultCacheManager(
                Paths.get("src", "main", "resources", "dist.xml").toString());
        ecm.start();
        managers.add(ecm);
        // Start the default cache
        ecm.getCache();
    }
} catch (Exception e) {
    e.printStackTrace();
}

Here is an example dist.xml file using TCPPING discovery for cluster formation:

<infinispan
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="urn:infinispan:config:14.0 http://www.infinispan.org/schemas/infinispan-config-14.0.xsd
                          urn:org:jgroups http://www.jgroups.org/schema/jgroups-5.2.xsd"
      xmlns="urn:infinispan:config:14.0"
      xmlns:ispn="urn:infinispan:config:14.0">
   <jgroups>
       <stack name="tcpping" extends="tcp">
           <MPING ispn:stack.combine="REMOVE" xmlns="urn:org:jgroups"/>
           <TCPPING async_discovery="true"
                    initial_hosts="${initial_hosts:127.0.0.1[7800],127.0.0.1[7801]}"
                    port_range="0" ispn:stack.combine="INSERT_AFTER" ispn:stack.position="TCP" xmlns="urn:org:jgroups"/>
       </stack>
   </jgroups>

   <cache-container name="test" default-cache="dist">
       <transport cluster="test" stack="tcpping"/>
      <distributed-cache name="dist">
         <memory>
            <max-size object-size="21000"/>
         </memory>
      </distributed-cache>
   </cache-container>
</infinispan>

Frequently Asked Questions (FAQs)

How does Quarkus 3.x handle ProtoStream schema registration for Infinispan?

In Quarkus 3.x, schema registration is automated during build time using the @AutoProtoSchemaBuilder annotation on an interface extending SerializationContextInitializer. Quarkus processes ProtoStream annotations, generates the Protobuf schema files, and registers them automatically with the Hot Rod client without requiring manually written .proto files.

What is the difference between Remote and Embedded Infinispan modes in Quarkus?

In Remote mode (quarkus-infinispan-client), Quarkus connects to an external Infinispan cluster using the lightweight Hot Rod client protocol, separating application lifecycle from cache storage. In Embedded mode (quarkus-infinispan-embedded), the Infinispan cache instance runs directly inside the JVM running Quarkus, sharing compute and memory resources with the application.

Can I use reactive non-blocking methods with RemoteCache in Quarkus?

Yes, Infinispan's Hot Rod client provides asynchronous methods (such as putAsync() and getAsync()) that return Java CompletableFuture instances. These can easily be transformed into Mutiny types like Uni or Multi in Quarkus RESTEasy Reactive endpoints to build completely non-blocking reactive pipelines.


Recommended Articles

Integrate PrimeFaces in Quarkus Applications with Jakarta EE 10

Learn how to integrate PrimeFaces library into Quarkus applications for Jakarta EE 10 environments.

A Comprehensive Comparison of WildFly Application Server and Quarkus Framework in Enterprise Java

Explore the features and use cases of WildFly and Quarkus for robust Java applications. #WildFly #Quarkus #EnterpriseJava

Mastering Quarkus Migrations: A Comprehensive Guide

Learn how to migrate your Quarkus applications with ease using our comprehensive migration guidelines and tools.

Create Standalone Quarkus Applications and Powerful Scripts Using JBang & Quarkus Command Mode

Learn how to develop standalone Quarkus applications with JBang and powerful scripts using Quarkus Command Mode. #Quarkus #Java #Microservices #CloudNative