IMap listeners as Spring beans: less wiring, clearer event handling
Hazelcast Toolkit series: Part 3.
IMap listeners as Spring beans: less wiring, clearer event handling
Hazelcast Toolkit series: Part 3.

Hazelcast IMap listeners are useful in the exact places where Spring applications already have structure: services, components, transactions, metrics, and domain-specific event handlers.
But the usual wiring often pulls listener registration away from the listener itself.
You define a listener class in one place, register it in a ClientConfig bean somewhere else, and then keep both sides synchronized as maps and event types change. It works, but it feels unlike the rest of a Spring Boot application.
Hazelcast Toolkit makes the listener itself the registration point:
@Component
@HzIMapListener(map = "users")
public class UserEvents implements EntryAddedListener<String, UserProfile> {
@Override
public void entryAdded(EntryEvent<String, UserProfile> event) {
log.info("User added: {}", event.getKey());
}
}
The bean is discovered after Spring finishes creating singletons, registered against the target IMap, and deregistered when the context shuts down.
The manual version
Manual listener registration usually looks like this:
@Bean
public HazelcastInstance hazelcastClient(UserEvents userEvents) {
ClientConfig config = new ClientConfig();
config.getNetworkConfig().addAddress("127.0.0.1:5701");
HazelcastInstance client = HazelcastClient.newHazelcastClient(config);
IMap<String, UserProfile> users = client.getMap("users");
users.addEntryListener(userEvents, true);
return client;
}
This is fine for one listener. It becomes noisy when you have several maps:
users.addEntryListener(userEvents, true);
sessions.addEntryListener(sessionEvents, false);
orders.addLocalEntryListener(orderEvents);
invoices.addEntryListener(invoiceEvents, true);
The registration list becomes an infrastructure registry that needs to know about application behavior. In a Spring app, the listener bean can describe that better itself.
The annotation-driven version
With Hazelcast Toolkit, a listener is a normal Spring bean plus one annotation:
package com.example.users;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.map.listener.EntryAddedListener;
import io.github.javaquasar.hazelcast.toolkit.annotation.HzIMapListener;
import org.springframework.stereotype.Component;
@Component
@HzIMapListener(map = "users")
public class UserCreatedListener implements EntryAddedListener<String, UserProfile> {
@Override
public void entryAdded(EntryEvent<String, UserProfile> event) {
// publish domain event, update read model, record metric, etc.
}
}
The annotation has three attributes:

Example with key-only events:
@Component
@HzIMapListener(map = "sessions", includeValue = false)
public class SessionTouchedListener implements EntryUpdatedListener<String, SessionState> {
@Override
public void entryUpdated(EntryEvent<String, SessionState> event) {
log.debug("Session changed: {}", event.getKey());
}
}
Example with a local-only listener:
@Component
@HzIMapListener(map = "orders", localOnly = true)
public class LocalOrderListener implements EntryRemovedListener<String, OrderSnapshot> {
@Override
public void entryRemoved(EntryEvent<String, OrderSnapshot> event) {
log.info("Local partition removed order {}", event.getKey());
}
}
When localOnly = true, Hazelcast registers the listener with IMap.addLocalEntryListener(…). In that mode the includeValue flag is ignored by Hazelcast.
What gets registered
The annotated bean must implement a Hazelcast listener type:
import com.hazelcast.map.listener.MapListener;
or:
import com.hazelcast.core.EntryListener;
The toolkit validates this during context startup. A bean annotated with ***@HzIMapListener ***but not implementing a listener interface fails fast instead of silently doing nothing.
Сonceptually, registration looks like this:
Map<String, Object> candidates =
beanFactory.getBeansWithAnnotation(HzIMapListener.class);
for (Object bean : candidates.values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
HzIMapListener metadata =
AnnotationUtils.findAnnotation(targetClass, HzIMapListener.class);
IMap<Object, Object> map = hazelcastInstance.getMap(metadata.map());
if (metadata.localOnly()) {
map.addLocalEntryListener((MapListener) bean);
} else {
map.addEntryListener((MapListener) bean, metadata.includeValue());
}
}
The real implementation also tracks listener IDs and removes them during shutdown.
Why this works well with Spring
Putting the listener registration on the bean has a few nice side effects.
First, the wiring is local. The class that handles users events says that it handles users events.
Second, the listener can use normal Spring dependencies:
@Component
@HzIMapListener(map = "users")
public class UserProjectionListener implements EntryAddedListener<String, UserProfile> {
private final UserProjection projection;
private final MeterRegistry meterRegistry;
public UserProjectionListener(UserProjection projection, MeterRegistry meterRegistry) {
this.projection = projection;
this.meterRegistry = meterRegistry;
}
@Override
public void entryAdded(EntryEvent<String, UserProfile> event) {
projection.create(event.getKey(), event.getValue());
meterRegistry.counter("users.cache.events", "type", "added").increment();
}
}
Third, AOP-proxied beans are handled by resolving the target class before reading the annotation. That matters if your listener also uses Spring features such as ***@Transactional***.
@Component
@HzIMapListener(map = "invoices")
public class InvoiceCacheListener implements EntryUpdatedListener<String, InvoiceSnapshot> {
@Transactional
@Override
public void entryUpdated(EntryEvent<String, InvoiceSnapshot> event) {
// The bean may be proxied, but the toolkit still finds @HzIMapListener.
}
}
Registration lifecycle
The listener lifecycle follows the Spring context lifecycle:

This avoids registering listeners too early, before dependencies are ready. It also avoids leaking listener registrations when a test context or application context is closed.
A complete example
Configuration:
spring:
application:
name: event-service
hazelcast:
client:
cluster-name: dev
network:
cluster-members:
- 127.0.0.1:5701
Cache value:
import io.github.javaquasar.hazelcast.toolkit.annotation.HzCompact;
@HzCompact
public class UserProfile {
private String userId;
private String displayName;
public UserProfile() {
}
public UserProfile(String userId, String displayName) {
this.userId = userId;
this.displayName = displayName;
}
public String getUserId() {
return userId;
}
public String getDisplayName() {
return displayName;
}
}
Listener:
import com.hazelcast.core.EntryEvent;
import com.hazelcast.map.listener.EntryAddedListener;
import io.github.javaquasar.hazelcast.toolkit.annotation.HzIMapListener;
import org.springframework.stereotype.Component;
@Component
@HzIMapListener(map = "users", includeValue = true)
public class UserProfileListener implements EntryAddedListener<String, UserProfile> {
@Override
public void entryAdded(EntryEvent<String, UserProfile> event) {
System.out.println("New user in cache: " + event.getValue().getDisplayName());
}
}
Producer:
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;
import org.springframework.stereotype.Service;
@Service
public class UserProfileCache {
private final IMap<String, UserProfile> users;
public UserProfileCache(HazelcastInstance hazelcastInstance) {
this.users = hazelcastInstance.getMap("users");
}
public void save(UserProfile profile) {
users.put(profile.getUserId(), profile);
}
}
No central listener registry. No separate config class just to call addEntryListener.
When to use includeValue=false
The default includeValue = true is convenient because event handlers can read the new value directly:
@HzIMapListener(map = "users", includeValue = true)
But values can be large. If the handler only needs the key, turn values off:
@HzIMapListener(map = "users", includeValue = false)
This reduces serialization and network overhead for distributed listener events. It is a good fit for invalidation, key-based refresh, counters, and async follow-up work.
When to use localOnly=true
Use localOnly = true when you only want events for partitions owned by the local Hazelcast member:
@HzIMapListener(map = "jobs", localOnly = true)
public class LocalJobListener implements EntryAddedListener<String, JobState> {
// ...
}
This can be useful for partition-aware processing and for reducing cross-member event traffic. It is not a replacement for a global event stream. If every service instance must observe every change, keep localOnly = false.
Operational notes
Listener code runs in the path of cache events, so keep handlers boring:
- avoid blocking calls inside listener methods;
- move slow work to a queue or executor;
- use includeValue = false when the value is not needed;
- make handlers idempotent where possible;
- log enough to diagnose event flow, but not on every hot-path event at INFO.
The annotation removes registration boilerplate. It does not remove the normal distributed-systems responsibility of designing event handlers carefully.
Resources
- GitHub repository: https://github.com/javaquasar/hazelcast-spring-toolkit
- Maven Central: https://central.sonatype.com/artifact/io.github.javaquasar/hazelcast-toolkit-spring-boot3/0.3.0
- Runnable Spring Boot 3 example: example-spring-boot3
Gradle:
implementation 'io.github.javaquasar:hazelcast-toolkit-spring-boot3:0.3.0'
Maven:
<dependency>
<groupId>io.github.javaquasar</groupId>
<artifactId>hazelcast-toolkit-spring-boot3</artifactId>
<version>0.3.0</version>
</dependency>
Closing thought
IMap listeners are application behavior. They should look like application behavior.
With ***@HzIMapListener***, listener registration becomes part of the Spring bean itself. The Hazelcast API is still there, but the repetitive wiring moves into the starter where it belongs.
Previous: Part 2 — Auto-registering Hazelcast Compact serialization in Spring Boot
Next: Part 4 — Hazelcast Hibernate L2 cache in real Spring Boot apps
메타데이터
- post_id
- 446cb4bbc3d0
- slug
- imap-listeners-as-spring-beans-less-wiring-clearer-event-handling-446cb4bbc3d0
- url
- https://medium.com/@artur.buzov/imap-listeners-as-spring-beans-less-wiring-clearer-event-handling-446cb4bbc3d0
- canonical_url
- https://medium.com/@artur.buzov/imap-listeners-as-spring-beans-less-wiring-clearer-event-handling-446cb4bbc3d0
- author_url
- https://medium.com/@artur.buzov
- status
- ok
- fetched_at
- 2026-07-24 15:22:40