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