infinispan

Infinispan Deployment.YAML on OpenShift Container Platform

Deploying Infinispan to OpenShift can be done easily by using either Operator or Helm chart. However, there is an even easier way to deploy it, and that is by using a single YAML file.

---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: infinispan
  namespace: cache
  labels:
    app: infinispan
spec:
  replicas: 1
  selector:
    matchLabels:
      app: infinispan
  template:
    metadata:
      labels:
        app: infinispan
    spec:
      volumes:
        - name: data-volume
          persistentVolumeClaim:
            claimName: ispn-pv
      containers:
        - name: infinispan
          image: 'quay.io/infinispan/server:16.1'
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              memory: 1Gi
            requests:
              memory: 1Gi
          env:
            - name: USER
              value: admin
            - name: PASS
              value: password
          ports:
            - name: infinispan
              containerPort: 11222
              protocol: TCP
          volumeMounts:
            - name: data-volume
              mountPath: /opt/infinispan/server/data
---
kind: Service
apiVersion: v1
metadata:
  name: infinispan
  namespace: cache
spec:
  ports:
    - protocol: TCP
      port: 11222
      targetPort: 11222
  selector:
    app: infinispan
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: ispn-pv
  namespace: cache
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeMode: Filesystem

We can deploy this single file directly to OpenShift and have it up and running in a few minutes. However this approach is perfect for local development, testing, or a quick proof-of-concept. If you intend to scale Infinispan into a multi-node distributed cluster for production, you should migrate this configuration to a StatefulSet or leverage the official Infinispan Operator to handle cluster discovery and data replication.

[Spring Boot] Implementing Multiple Cache Managers

Typically, we leverage a single cache for a specific purpose. however, there are scenarios where we need to integrate multiple caching mechanisms, each serving a different role. In this tutorial, I will demonstrate how to configure multiple Cache Managers in Spring Boot and illustrate the integration between the application layer, database, local cache, and remote cache.

In this scenario, we will use both a local cache (Caffeine) and a remote cache (Infinispan) to store query results.

First we’ll start with a simple pom.xml which contains Spring Boot, MySQL, Infinispan server, and Caffeine for local caches.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>spring-boot-multiple-cacheable</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.infinispan</groupId>
                <artifactId>infinispan-bom</artifactId>
                <version>16.0.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- caches -->
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring-boot3-starter-remote</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-jboss-marshalling</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>JAR</layout>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

its configuration on application.properties

### server
server.port=8080
spring.application.name=spring-boot-multiple-cacheable

## log
logging.level.root=INFO
logging.level.com.edw=DEBUG

spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE

## db
spring.datasource.url=jdbc:mysql://localhost:3306/test_db
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=password

## infinispan
jdg.servers=localhost:11222
jdg.userName=admin2
jdg.password=password

## caffeine
caffeine.spec=maximumSize=500,expireAfterAccess=10m

And some Java files,

package com.edw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
package com.edw.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.infinispan.api.annotations.indexing.Indexed;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor

@Indexed

@Entity
@Table(name = "t_employee")
public class Employee implements Serializable {
    @Id
    public Long id;

    @Column(name = "gender")
    public String gender;

    @Column(name = "firstname")
    public String firstname;

    @Column(name = "lastname")
    public String lastname;
}
package com.edw.repository;

import com.edw.model.Employee;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
    List<Employee> findTop10ByFirstnameLikeAndLastnameLikeIgnoreCase(String firstname, String lastname);
}
package com.edw.service;

import com.edw.model.Employee;
import com.edw.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(@Autowired EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Cacheable(value = "query-cache",
            key = "#id",
            cacheManager = "infinispanCacheManager")
    public Employee getEmployee(Long id) {
        return employeeRepository.findById(id).orElse(new Employee());
    }

    @Cacheable(value = "employee-cache",
            key = "#root.methodName + '-' + #firstname + '-' +  #lastname",
            cacheManager = "caffeineCacheManager")
    public List<Employee> findEmployeesByFirstnameAndLastname(String firstname, String lastname) {
        return employeeRepository.findTop10ByFirstnameLikeAndLastnameLikeIgnoreCase(firstname + "%", lastname + "%");
    }
}
package com.edw.controller;

import com.edw.model.Employee;
import com.edw.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
public class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(@Autowired EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @GetMapping("/employee/{id}")
    public ResponseEntity getEmployeeById (@PathVariable Long id) {
        log.info("getEmployeeById");

        Employee employee = employeeService.getEmployee(id);
        if(employee.getId() == null)
            return ResponseEntity.notFound().build();

        return ResponseEntity.ok(employee);
    }

    @GetMapping("/employee/find-by-firstname-and-lastname/{firstname}/{lastname}")
    public ResponseEntity getEmployeeByFirstnameAndLastname (@PathVariable String firstname, @PathVariable String lastname) {
        log.info("getEmployeeByFirstnameAndLastname");

        List<Employee> employees = employeeService.findEmployeesByFirstnameAndLastname(firstname, lastname);
        if(employees.isEmpty())
            return ResponseEntity.notFound().build();

        return ResponseEntity.ok(employees);
    }

}

And this is the most important part, where we configure our Cache connections. We use @Primary on the Infinispan manager to set it as the default, but we use explicit bean names (caffeineCacheManager and infinispanCacheManager) to distinguish them in the service layer.

package com.edw.config;

import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.ClientIntelligence;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.jboss.marshalling.commons.GenericJBossMarshaller;
import org.infinispan.spring.remote.provider.SpringRemoteCacheManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class InfinispanConfiguration {
    @Value("${jdg.servers}")
    private String jdgHostAddress;

    @Value("${jdg.userName}")
    private String userName;

    @Value("${jdg.password}")
    private String password;

    @Primary
    @Bean(name="infinispanCacheManager")
    public CacheManager infinispanCacheManager() {
        return new SpringRemoteCacheManager(remoteCacheManager());
    }

    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers(jdgHostAddress)
                        .security().authentication().username(userName).password(password)
                        .clientIntelligence(ClientIntelligence.HASH_DISTRIBUTION_AWARE)
                        .marshaller(new GenericJBossMarshaller())
                        .build());
    }
}
package com.edw.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfiguration {

    @Value("${caffeine.spec}")
    private String spec;

    @Bean(name = "caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager("employee-cache");
        manager.setCaffeine(Caffeine.from(spec));
        return manager;
    }

}

We can try to trigger API call and can see that the first hit will go to the database, but after that it will always go to Cache to reduce the database workload.

$ curl -kv http://localhost:8080/employee/1

Code for this project can be accessed in the below repository,

https://github.com/edwin/spring-boot-multiple-cacheable

Creating a Custom Cache Listener on Infinispan

Infinispan offers powerful caching capabilities, but sometimes you need to trigger custom business logic when cache events occur, such as a data entry expiring. This post demonstrates how to create and deploy a custom Infinispan Cache Listener integrated via a Module Lifecycle component.

First, let’s configure a cache named user-cache with a short lifespan to easily demonstrate the expiration event.

For this example, the data will persist in the Infinispan memory for 5 seconds from the latest transaction involving that data, which is set by the maxIdle property (in milliseconds).

user-cache: 
  replicatedCache: 
    mode: "SYNC"
    statistics: "true"
    encoding: 
      mediaType: "text/plain"
    expiration: 
      lifespan: "-1"
      maxIdle: "5000"

We want to trigger an action every time an entry in the user-cache is created, modified, removed, or, most importantly for this case, expired. We achieve this with a custom class annotated with @Listener.

For this sample, the listener specifically checks if the event belongs to the user-cache before printing a message.

package com.edw;

import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.*;
import org.infinispan.notifications.cachelistener.event.*;

@Listener(clustered = true, observation = Listener.Observation.POST)
public class HelloWorldListener {

    @CacheEntryCreated
    public void entryCreated(CacheEntryCreatedEvent<Object, Object> event) {
        if("user-cache".equals(event.getCache().getName()))
            System.out.println("entryCreated for user-cache with key is " + event.getKey() + " and value is " + event.getValue());
    }

    @CacheEntryModified
    public void entryModified(CacheEntryModifiedEvent<String, String> event) {
        if("user-cache".equals(event.getCache().getName()))
            System.out.println("entryModified for user-cache with key is " + event.getKey() + " and new value is " + event.getNewValue());
    }

    @CacheEntryRemoved
    public void entryRemoved(CacheEntryRemovedEvent<String, String> event) {
        if("user-cache".equals(event.getCache().getName()))
            System.out.println("entryRemoved for user-cache with key is " + event.getKey() + " and value is " + event.getValue());
    }

    @CacheEntryExpired
    public void entryExpired(CacheEntryExpiredEvent<String, String> event) {
        if("user-cache".equals(event.getCache().getName()))
            System.out.println("entryExpired for user-cache with key is " + event.getKey() + " and value is " + event.getValue());
    }
}

To automatically register our listener when Infinispan starts, we’ll create a custom Infinispan Module that implements the ModuleLifecycle interface.

The cacheStarted method is the perfect place to check the cache name and register the listener once the user-cache has been initialized.

package com.edw;

import org.infinispan.Cache;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.factories.ComponentRegistry;
import org.infinispan.factories.GlobalComponentRegistry;
import org.infinispan.factories.annotations.InfinispanModule;
import org.infinispan.lifecycle.ModuleLifecycle;

@InfinispanModule(name = "custom-module", requiredModules = "core")
public class CustomModule implements ModuleLifecycle {
   @Override
   public void cacheManagerStarting(GlobalComponentRegistry gcr, GlobalConfiguration globalConfiguration) {
      CustomModuleConfiguration config = globalConfiguration.module(CustomModuleConfiguration.class);
      if (config != null) {
         System.out.println("Custom Module Message: " + config.message());
      }
   }

   @Override
   public void cacheStarted(ComponentRegistry cr, String cacheName) {
      System.out.println("Cache " + cacheName + " started!");
      if("user-cache".equals(cacheName))
         cr.getComponent(Cache.class).addListener(new HelloWorldListener());
   }
}

The remaining Java classes are necessary boilerplate codes for defining a custom configuration element that can be read by Infinispan’s parser.

package com.edw;

import java.util.HashMap;
import java.util.Map;

public enum Attribute {
   // must be first
   UNKNOWN(null),

   MESSAGE("message");
   private static final Map<String, Attribute> ATTRIBUTES;

   static {
      final Map<String, Attribute> map = new HashMap<>();
      for (Attribute attribute : values()) {
         final String name = attribute.name;
         if (name != null) {
            map.put(name, attribute);
         }
      }
      ATTRIBUTES = Map.copyOf(map);
   }

   private final String name;

   Attribute(final String name) {
      this.name = name;
   }

   public static Attribute forName(String localName) {
      final Attribute attribute = ATTRIBUTES.get(localName);
      return attribute == null ? UNKNOWN : attribute;
   }

   @Override
   public String toString() {
      return name;
   }
}
package com.edw;

import org.infinispan.commons.configuration.BuiltBy;
import org.infinispan.commons.configuration.attributes.AttributeDefinition;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.commons.configuration.attributes.ConfigurationElement;
import org.infinispan.configuration.serializing.SerializedWith;

@BuiltBy(CustomModuleConfigurationBuilder.class)
@SerializedWith(CustomModuleSerializer.class)
public class CustomModuleConfiguration extends ConfigurationElement<CustomModuleConfiguration> {

   static final AttributeDefinition<String> MESSAGE = AttributeDefinition.builder(Attribute.MESSAGE, "Module Loaded")
         .immutable().build();
   static AttributeSet attributeDefinitionSet() {
      return new AttributeSet(CustomModuleConfiguration.class, MESSAGE);
   }

   CustomModuleConfiguration(AttributeSet attributes) {
      super(Element.ROOT, attributes);
   }

   public String message() {
      return attributes.attribute(MESSAGE).get();
   }
}
package com.edw;

import org.infinispan.commons.configuration.Builder;
import org.infinispan.commons.configuration.Combine;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;

public class CustomModuleConfigurationBuilder implements Builder<CustomModuleConfiguration> {

   private final AttributeSet attributes = CustomModuleConfiguration.attributeDefinitionSet();

   private final GlobalConfigurationBuilder builder;

   public CustomModuleConfigurationBuilder(GlobalConfigurationBuilder builder) {
      this.builder = builder;
   }

   @Override
   public CustomModuleConfiguration create() {
      return new CustomModuleConfiguration(attributes.protect());
   }

   @Override
   public Builder<?> read(CustomModuleConfiguration template, Combine combine) {
      this.attributes.read(template.attributes(), combine);
      return this;
   }

   @Override
   public AttributeSet attributes() {
      return attributes;
   }

   public Builder<?> message(String message) {
      attributes.attribute(CustomModuleConfiguration.MESSAGE).set(message);
      return this;
   }
}
package com.edw;

import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.commons.configuration.io.ConfigurationReader;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.configuration.parsing.ConfigurationParser;
import org.infinispan.configuration.parsing.Namespace;
import org.infinispan.configuration.parsing.ParseUtils;
import org.infinispan.configuration.parsing.Parser;
import org.infinispan.configuration.parsing.ParserScope;
import org.kohsuke.MetaInfServices;

@MetaInfServices
@Namespace(root = "custom-module")
@Namespace(uri = Parser.NAMESPACE + "*", root = "custom-module")
public class CustomModuleParser implements ConfigurationParser {

   @Override
   public void readElement(ConfigurationReader reader, ConfigurationBuilderHolder holder) {
      if (!holder.inScope(ParserScope.CACHE_CONTAINER))
         throw new CacheConfigurationException(String.format("Unexpected scope. Expected CACHE_CONTAINER but was %s", holder.getScope()));


      Element element = Element.forName(reader.getLocalName());
      if (element != Element.ROOT)
         throw ParseUtils.unexpectedElement(reader);

      GlobalConfigurationBuilder globalBuilder = holder.getGlobalConfigurationBuilder();
      CustomModuleConfigurationBuilder builder = globalBuilder.addModule(CustomModuleConfigurationBuilder.class);

      for (int i = 0; i < reader.getAttributeCount(); i++) {
         ParseUtils.requireNoNamespaceAttribute(reader, i);
         String value = reader.getAttributeValue(i);
         Attribute attribute = Attribute.forName(reader.getAttributeName(i));
         switch (attribute) {
            case MESSAGE:
               builder.message(value);
               break;
            default:
               throw ParseUtils.unexpectedAttribute(reader, i);
         }
      }
      ParseUtils.requireNoContent(reader);
   }

   @Override
   public Namespace[] getNamespaces() {
      return ParseUtils.getNamespaceAnnotations(getClass());
   }
}
package com.edw;

import org.infinispan.commons.configuration.io.ConfigurationWriter;
import org.infinispan.configuration.serializing.ConfigurationSerializer;

public class CustomModuleSerializer implements ConfigurationSerializer<CustomModuleConfiguration> {
   @Override
   public void serialize(ConfigurationWriter writer, CustomModuleConfiguration configuration) {
      writer.writeStartElement(Element.ROOT);
      configuration.attributes().write(writer);
      writer.writeEndElement();
   }
}
package com.edw;

import java.util.HashMap;
import java.util.Map;

public enum Element {
   //must be first
   UNKNOWN(null),

   ROOT("custom-module"),
   ;

   private static final Map<String, Element> ELEMENTS;

   static {
      final Map<String, Element> map = new HashMap<>();
      for (Element element : values()) {
         final String name = element.name;
         if (name != null) {
            map.put(name, element);
         }
      }
      ELEMENTS = Map.copyOf(map);
   }

   private final String name;

   Element(final String name) {
      this.name = name;
   }

   public static Element forName(final String localName) {
      final Element element = ELEMENTS.get(localName);
      return element == null ? UNKNOWN : element;
   }

   @Override
   public String toString() {
      return name;
   }
}

We need to include the core Infinispan dependencies and the necessary artifacts for component annotation processing. The use of infinispan-bom ensures dependency versions are consistent.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>infinispan-cache-listener</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.infinispan</groupId>
                <artifactId>infinispan-bom</artifactId>
                <version>16.0.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-component-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-component-processor</artifactId>
        </dependency>
    </dependencies>

</project>

Execute the following commands to build the project and deploy the resulting JAR file into the Infinispan server’s library folder.

$ mvn clean package

$ cp infinispan-cache-listener-1.0.0.jar ${ISPN_HOME}/server/lib

Start Infinispan. You should see the custom module message and the confirmation that your specific cache started.

2025-11-23 14:33:09,598 INFO  [o.i.CLUSTER] ISPN000094: Received new cluster view for channel cluster: [DESKTOP-8NNFTMF-10079|0] (1) [DESKTOP-8NNFTMF-10079]
2025-11-23 14:33:09,660 INFO  [o.i.CLUSTER] ISPN000079: Channel `cluster` local address is `DESKTOP-8NNFTMF-10079`, physical addresses are `[192.168.8.120:7800]`
2025-11-23 14:33:09,660 INFO  [o.i.CONTAINER] ISPN000389: Loaded global state, version=15.0.7.Final timestamp=2025-11-23T07:32:46.562161300Z

// ... other caches ..

Cache user-cache started!

// ... server started logs ...

2025-11-23 14:33:10,639 INFO  [o.i.SERVER] ISPN080018: Started connector Resp (internal)
2025-11-23 14:33:10,639 INFO  [o.i.SERVER] ISPN080018: Started connector Memcached (internal)
Cache ___hotRodTopologyCache_hotrod-default started!
2025-11-23 14:33:10,647 INFO  [o.i.SERVER] ISPN080018: Started connector HotRod (internal)
2025-11-23 14:33:10,741 INFO  [o.i.SERVER] ISPN080018: Started connector REST (internal)
2025-11-23 14:33:10,741 INFO  [o.i.SERVER] ISPN005055: Using transport: NIO
2025-11-23 14:33:10,823 INFO  [o.i.SERVER] ISPN080004: Connector SinglePort (default) listening on 127.0.0.1:11222
2025-11-23 14:33:10,823 INFO  [o.i.SERVER] ISPN080034: Server 'DESKTOP-8NNFTMF-10079' listening on http://127.0.0.1:11222
2025-11-23 14:33:10,854 INFO  [o.i.SERVER] ISPN080001: Infinispan Server 15.0.7.Final started in 5283ms

Try adding new data on “user-cache”

We can see this logs related to cache addition and expiry on out ISPN logs

entryCreated for user-cache with key is user one and value is value one
entryExpired for user-cache with key is user one and value is value one

Code for this project can be found on the below repository,The complete source code for this project is available on the following repository,

https://github.com/edwin/infinispan-cache-listener

Error “No marshaller registered for object of Java type” in Infinispan

Had this error whenc trying to put a Java bean to Infinispan 15,

java.lang.IllegalArgumentException: No marshaller registered for object of Java type com.edw.model.User : com.edw.model.User@1a8d9c94
	at org.infinispan.protostream.impl.SerializationContextImpl.getMarshallerDelegate(SerializationContextImpl.java:517) ~[protostream-5.0.4.Final.jar:5.0.4.Final]
	at org.infinispan.protostream.WrappedMessage.writeCustomObject(WrappedMessage.java:300) ~[protostream-5.0.4.Final.jar:5.0.4.Final]
	at org.infinispan.protostream.WrappedMessage.writeMessage(WrappedMessage.java:250) ~[protostream-5.0.4.Final.jar:5.0.4.Final]
	at org.infinispan.protostream.WrappedMessage.write(WrappedMessage.java:243) ~[protostream-5.0.4.Final.jar:5.0.4.Final]
	at org.infinispan.protostream.ProtobufUtil.toWrappedByteBuffer(ProtobufUtil.java:152) ~[protostream-5.0.4.Final.jar:5.0.4.Final]
	at org.infinispan.commons.marshall.ImmutableProtoStreamMarshaller.objectToBuffer(ImmutableProtoStreamMarshaller.java:55) ~[infinispan-commons-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.commons.marshall.AbstractMarshaller.objectToByteBuffer(AbstractMarshaller.java:70) ~[infinispan-commons-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.client.hotrod.marshall.MarshallerUtil.obj2bytes(MarshallerUtil.java:117) ~[infinispan-client-hotrod-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.client.hotrod.DataFormat$DataFormatImpl.valueToBytes(DataFormat.java:92) ~[infinispan-client-hotrod-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.client.hotrod.DataFormat.valueToBytes(DataFormat.java:211) ~[infinispan-client-hotrod-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.client.hotrod.impl.RemoteCacheImpl.valueToBytes(RemoteCacheImpl.java:628) ~[infinispan-client-hotrod-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.client.hotrod.impl.RemoteCacheImpl.putAsync(RemoteCacheImpl.java:315) ~[infinispan-client-hotrod-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.client.hotrod.impl.RemoteCacheSupport.put(RemoteCacheSupport.java:196) ~[infinispan-client-hotrod-15.0.7.Final.jar:15.0.7.Final]
	at org.infinispan.client.hotrod.impl.RemoteCacheSupport.put(RemoteCacheSupport.java:186) ~[infinispan-client-hotrod-15.0.7.Final.jar:15.0.7.Final]

Actually it happen because of my Java bean (com.edw.model.User) doesnt have any marshaller. This is how my configuration files looks like,

@Configuration
public class InfinispanConfiguration {
    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new org.infinispan.client.hotrod.configuration.ConfigurationBuilder()
                        .addServers("localhost:11222")
                        .security().authentication().username("admin2").password("password")
                        .clientIntelligence(ClientIntelligence.HASH_DISTRIBUTION_AWARE)
                        .marshaller(ProtoStreamMarshaller.class)
                        .build());
    }
}

And everything works well after i register a marshaller for User bean

@Configuration
public class InfinispanConfiguration {
    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new org.infinispan.client.hotrod.configuration.ConfigurationBuilder()
                        .addServers("localhost:11222")
                        .security().authentication().username("admin2").password("password")
                        .clientIntelligence(ClientIntelligence.HASH_DISTRIBUTION_AWARE)
                        .marshaller(ProtoStreamMarshaller.class)
                        .addContextInitializer(new UserIndexSchemaInitializerImpl())
                        .build());
    }
}

Where UserIndexSchemaInitializerImpl is a generated code coming from interface that extending SerializationContextInitializer class.

Full code for this can be found on my Github repository,

https://github.com/edwin/spring-boot-2-and-infinispan-15

Deploying Red Hat DataGrid 8 using Operator to Openshift 4 and Setting Up a Persistent Cache

Red Hat Data Grid, or its Open Source version which is Infinispan, is a good distributed cache and key-value NoSQL data store software developed by Red Hat which can be used as an embedded library or as a standalone server. It even can be deployed easily to a container management platform like Openshift by using Operator or Helm chart. And for this example, we are trying to deploy Red Hat Data Grid 8.4 to Openshift by using Operator.

First lets start by creating a namespace dedicated for Data Grid

$ oc new-project datagrid-ns

And create two new YAML file, one is datagrid-operator.yaml

apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
 name: datagrid
 namespace: datagrid-ns

and another one is datagrid-subscription.yaml

apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
 name: datagrid-operator
 namespace: datagrid-ns
spec:
 channel: 8.4.x
 installPlanApproval: Manual
 name: datagrid
 source: redhat-operators
 sourceNamespace: openshift-marketplace

apply them,

$ oc apply -f datagrid-operator.yaml
$ oc apply -f datagrid-subscription.yaml

it would generate a new datagrid item on “Installed Operators” page,

select “Upgrade” and “Approve” to install Data Grid Operator

a successful installation is going to looks like this,

next is creating a new Infinispan cluster, this is for sample purpose only therefore we disable TLS certificate to connect with a very minimum CPU and memory

kind: Infinispan
apiVersion: infinispan.org/v1
metadata:
  name: datagrid-cluster
  namespace: datagrid-ns
spec:
  replicas: 1
  security:
    endpointEncryption:
      type: None
  container:
    cpu: "500m:100m"
    memory: "1Gi:500Mi"

a successful configuration it would generate pods like this,

next is to expose its endpoint so it can be accessible from external

$ oc create route edge --service datagrid-cluster --hostname=datagrid.apps-crc.testing

the result is looks like this,

we can login by using credentials which is stored in Openshift’s Secret. If we are able to successfully login, we can see below Data Grid dashboard

Now lets focus on the Java part. For this, we are using Spring Boot and Java 17 which is defined in our pom.xml file

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>spring-boot-with-datagrid-operator</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <version.infinispan>14.0.2.Final</version.infinispan>
        <version.protostream>4.6.2.Final</version.protostream>
        <version.spring.boot3>3.0.4</version.spring.boot3>

        <start-class>com.edw.Main</start-class>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.infinispan</groupId>
                <artifactId>infinispan-bom</artifactId>
                <version>${version.infinispan}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${version.spring.boot3}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- infinispan -->
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring-boot-starter-remote</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-jboss-marshalling</artifactId>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-remote-query-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-api</artifactId>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring-boot-starter-embedded</artifactId>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.transaction</groupId>
            <artifactId>jta</artifactId>
            <version>1.1</version>
        </dependency>

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${version.spring.boot3}</version>
                <configuration>
                    <layout>JAR</layout>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

a Java file for configuration,

package com.edw.configuration;

import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.ClientIntelligence;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.jboss.marshalling.commons.GenericJBossMarshaller;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class InfinispanConfiguration {
    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers("datagrid-cluster.datagrid-ns.svc.cluster.local:11222")
                        .security().authentication().username("developer").password("password")
                        .clientIntelligence(ClientIntelligence.HASH_DISTRIBUTION_AWARE)
                        .marshaller(new GenericJBossMarshaller())
                        .addJavaSerialWhiteList(".*")
                        .build());
    }
}

And this is perhaps most important class where we can define our caches. For this example, we are trying to create two different caches where one cache is persistent and another one is not.

package com.edw.helper;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.commons.configuration.XMLStringConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.UUID;

@Service
public class CacheHelper {

    private RemoteCacheManager cacheManager;

    @Autowired
    public CacheHelper (RemoteCacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    public void populate() {
        // build cache without persistence
        RemoteCache cacheWithoutPersistence = cacheManager.administration().getOrCreateCache("cache-without-persistence",
                new XMLStringConfiguration("<distributed-cache name=\"cache-without-persistence\" mode=\"ASYNC\">\n" +
                        "\t<encoding media-type=\"application/x-jboss-marshalling\" />\n" +
                        "</distributed-cache>")
        );
        for (int i = 0; i < 50; i++) {
            cacheWithoutPersistence.put("key"+i, UUID.randomUUID().toString());
        }

        // build cache with persistence
        RemoteCache cacheWithPersistence = cacheManager.administration().getOrCreateCache("cache-with-persistence",
                new XMLStringConfiguration("<distributed-cache name=\"cache-with-persistence\" mode=\"ASYNC\">\n" +
                        "\t<encoding media-type=\"application/x-jboss-marshalling\" />\n" +
                        "\t<persistence passivation=\"false\">\n" +
                        "\t\t<file-store>\n" +
                        "\t\t  <index path=\"/opt/infinispan/server/data\" />\n" +
                        "\t\t  <data path=\"/opt/infinispan/server/data\" />\n" +
                        "\t\t</file-store>\n" +
                        "\t</persistence>\n" +
                        "</distributed-cache>")
        );
        for (int i = 0; i < 50; i++) {
            cacheWithPersistence.put("key"+i, UUID.randomUUID().toString());
        }
    }

    public HashMap getCacheWithoutPersistence() {
        RemoteCache<String, String> cache = cacheManager.getCache("cache-without-persistence");

        HashMap hashMap = new HashMap();
        for (Object key : cache.keySet()) {
            hashMap.put(key, cache.get(key));
        }
        return hashMap;
    }

    public HashMap getCacheWithPersistence() {
        RemoteCache<String, String> cache = cacheManager.getCache("cache-with-persistence");

        HashMap hashMap = new HashMap();
        for (Object key : cache.keySet()) {
            hashMap.put(key, cache.get(key));
        }
        return hashMap;
    }
}

with one controller file,

package com.edw.controller;

import com.edw.helper.CacheHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
public class IndexController {

    private CacheHelper cacheHelper;

    @Autowired
    public IndexController (CacheHelper cacheHelper) {
        this.cacheHelper = cacheHelper;
    }

    @GetMapping(path = "/cache-without-persistence")
    public HashMap cacheWithoutPersistence() {
        return cacheHelper.getCacheWithoutPersistence();
    }

    @GetMapping(path = "/cache-with-persistence")
    public HashMap cacheWithPersistence() {
        return cacheHelper.getCacheWithPersistence();
    }

    @GetMapping(path = "/populate")
    public HashMap populate() {
        cacheHelper.populate();
        return new HashMap() {{
            put("status", "success");
        }};
    }
}

After that we can build and deploy our Java application to Openshift,

Trigger populate endpoint from our Spring Boot to generate Caches and its contents,

$ curl -kv http://localhost:8080/populate
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /populate HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.76.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 14 Sep 2024 17:22:03 GMT
< 
* Connection #0 to host localhost left intact
{"status":"success"}

We can check the content of each Caches stores,

Now lets try to delete Data Grid pod and see whether all the cache data is gone or not,

$ oc delete po --grace-period=0 --force datagrid-cluster-0

We can see that cache-with-persistence Cache Store still having its data

while cache-without-persistence Cache Store is not having any data at all

Source code for this tutorial can be found here,

https://github.com/edwin/spring-boot-with-datagrid-operator