Hazelcast Cache in Spring Boot: A Practical Guide
Hazelcast is a common choice for distributed caching in Spring Boot applications: add it to the classpath, annotate a method with @Cacheable, and cached values are shared across every instance of your service rather than sitting in each JVM's own heap.
Getting it right beyond that first step takes a bit more care. This guide covers how Hazelcast caching actually works in Spring Boot — the three layers you can access it through, what Spring Boot's auto-configuration decides on your behalf, per-entry TTL, near cache, and the problems teams hit in production. At the end, it shows what the same setup looks like on Valkey or Redis, for anyone weighing the two.
Three Ways to Reach a Hazelcast Cache
Most confusion about Hazelcast caching comes from not knowing which of three layers you're using. They behave differently, and the one you pick determines which TTL and near-cache options are available to you.
IMapdirectly. Hazelcast's distributed map, which implementsjava.util.concurrent.ConcurrentMap. Full access to per-entry TTL, near cache, entry processors, and everything else. The most capable option and the least portable.- JCache (JSR-107). Hazelcast implements the standard Java caching API through
ICache. Portable across providers, at the cost of Hazelcast-specific features. - Spring's
CacheManagerabstraction. What@Cacheableuses. The least code and the most portable — you can change providers without touching call sites — but it exposes the smallest feature surface. Notably, no per-entry TTL.
These aren't mutually exclusive. A common and sensible pattern is @Cacheable for the bulk of your caching, dropping to IMap for the handful of caches that need per-entry expiry or explicit control.
Adding Hazelcast to a Spring Boot Application
You need Hazelcast itself, Spring's cache starter, and the hazelcast-spring module that supplies HazelcastCacheManager:
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Keep the hazelcast-spring version aligned with your Hazelcast version. If you're on Hazelcast Enterprise, you also need to exclude the transitive com.hazelcast:hazelcast dependency from hazelcast-spring, or you'll end up with both the open-source and enterprise jars on the classpath.
The Decision Spring Boot Makes for You
This is the part worth understanding before anything else, because Spring Boot's auto-configuration quietly picks your topology and the default is not what most people assume.
Spring Boot tries to create a client first, checking in this order:
- A
ClientConfigbean - A configuration file named by the
spring.hazelcast.configproperty - The
hazelcast.client.configsystem property hazelcast-client.xmlin the working directory or at the classpath roothazelcast-client.yaml(or.yml) in the same locations
Only if a client cannot be created does Spring Boot fall back to configuring an embedded member, looking for a Config bean, then hazelcast.xml or its YAML counterpart in the working directory or classpath root, then the hazelcast.config system property.
The practical consequence: if you drop Hazelcast on the classpath with a hazelcast.yaml and no client config, every instance of your application becomes a cluster member. Your app processes now hold the data and its backups, join and leave the cluster on every deploy, and participate in partition rebalancing. That's a legitimate topology — it's the fastest possible read path, since data can live in the same JVM — but it's a significant architectural commitment to make by accident.
If you want client/server instead, supply a hazelcast-client.yaml:
hazelcast-client:
cluster-name: dev
network:
cluster-members:
- hazelcast-0.hazelcast.default.svc.cluster.local:5701
- hazelcast-1.hazelcast.default.svc.cluster.local:5701
Once a HazelcastInstance is auto-configured, Spring Boot wraps it in a CacheManager automatically. If you'd rather keep caching on a separate instance from the rest of your Hazelcast usage, point spring.cache.hazelcast.config at its own configuration file.
Wiring the Cache Manager Explicitly
For explicit control, declare the cache manager yourself:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new HazelcastCacheManager(hazelcastInstance);
}
}
That's com.hazelcast.spring.cache.HazelcastCacheManager, from the hazelcast-spring module.
Using @Cacheable
With the cache manager in place, the Spring Cache annotations work as they do with any provider. Each named cache becomes a Hazelcast IMap of the same name.
@Service
public class ProductService {
@Cacheable("products")
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}
@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
return productRepository.save(product);
}
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
productRepository.deleteById(id);
}
}
Because the cache is a distributed map, an eviction on one instance is visible to all of them — which is the entire reason to use a distributed cache over a local one.
TTL and Eviction
Expiry is configured per map, in your Hazelcast configuration rather than in Spring:
hazelcast:
map:
default:
backup-count: 1
in-memory-format: BINARY
products:
time-to-live-seconds: 3600
max-idle-seconds: 600
eviction:
eviction-policy: LRU
max-size-policy: PER_NODE
size: 10000
The two expiry settings are easy to confuse:
time-to-live-secondsis relative to the entry's last write. An entry is removed if it hasn't been written to within the period.max-idle-secondsis relative to the last access — aget,put, orcontainsKeyall count as touching the entry.
Both default to 0, meaning infinite, and both can be set at once — an entry expires as soon as either policy says so. Map names support wildcards, so a configuration named session-* applies to every map whose name starts with that prefix.
Per-Entry TTL
Here's the limitation most teams hit: Spring's cache abstraction has no concept of per-entry TTL. You can't express "cache this particular value for five minutes" through @Cacheable. If you need it, drop to IMap:
IMap<Long, Product> products = hazelcastInstance.getMap("products");
// TTL only
products.put(1L, product, 10, TimeUnit.MINUTES);
// TTL and max-idle together
products.put(1L, product, 10, TimeUnit.MINUTES, 2, TimeUnit.MINUTES);
// change the TTL of an entry that already exists
products.setTtl(1L, 30, TimeUnit.MINUTES);
This mixing of annotation-driven and programmatic caching in the same codebase is normal with Hazelcast, but it does mean your cache invalidation logic ends up split across two styles.
Near Cache
A near cache keeps recently used entries in the process that requested them, so repeat reads don't cross the network. It matters most in client/server topology, where every plain get() is a round trip.
hazelcast-client:
near-cache:
products:
in-memory-format: BINARY
invalidate-on-change: true
time-to-live-seconds: 0
max-idle-seconds: 60
eviction:
size: 1000
max-size-policy: ENTRY_COUNT
eviction-policy: LFU
Note the placement: this goes in hazelcast-client.yaml, not the member configuration, and the near-cache name must match the name of the map on the member. Wildcards work, so product* would cover several maps at once. Members can also have near caches, configured on the member side, but the client case is the common one.
Three details worth knowing:
invalidate-on-changedefaults totrue, so local copies are evicted when the shared entry is updated or removed elsewhere. Turning it off gives you faster reads and stale data.- The near cache also stores null results. If you
get()a key that doesn't exist, that absence is cached, and subsequent lookups for the same missing key are served locally without a round trip. Useful under key-miss-heavy load, occasionally surprising. in-memory-format: NATIVEuses the High-Density Memory Store, which is Hazelcast Enterprise only.BINARYandOBJECTare available in Community Edition.
Problems Teams Actually Hit
Four issues account for most of the trouble in production.
Setting spring.cache.type=hazelcast and getting no cache manager. This property selects Spring's cache abstraction implementation, so it expects a HazelcastCacheManager bean in the context. It does not route through JCache, even though Hazelcast is JSR-107 compliant. Without that bean you get No cache manager could be auto-configured, check your configuration (caching type is 'HAZELCAST'). Either declare the bean as shown above, or set spring.cache.type=jcache and put javax.cache:cache-api on the classpath to use the JCache path instead.
Discovery failing in Kubernetes. Hazelcast's default multicast discovery does not work in most Kubernetes networks. You need the Kubernetes discovery plugin with a service name, or explicit member addresses. This bites hardest in embedded mode, where a discovery failure means each pod silently forms its own single-member cluster — the application works, the cache hit rate collapses, and nothing logs an error.
Serialization errors on cached types. Anything you cache has to be serializable under whichever mechanism you've configured — Java serialization, IdentifiedDataSerializable, Portable, or Compact. A JPA entity with lazy associations is a frequent offender, because the proxy gets dragged in with it.
Cluster churn on rolling deploys. In embedded mode, every deploy removes and adds cluster members, triggering partition rebalancing while your application is also handling traffic. Larger datasets make this more visible. Client/server topology avoids it entirely, which is one of the strongest arguments for not accepting the embedded default.
Community vs. Enterprise
Worth checking before a production rollout, because several caching-adjacent features sit behind the commercial license.
Hazelcast Community Edition is covered by the Apache License 2.0 together with the Hazelcast Community License. TLS, authentication, and encryption are Enterprise-only, as are the High-Density Memory Store (the NATIVE in-memory format), WAN replication, and persistence. Community Edition also receives CVE fixes only in major and minor releases, whereas Enterprise customers get patches as they're ready.
None of that makes Community Edition unusable — a great many teams run it happily — but "we'll use the open-source edition" and "we need encrypted transport" are not compatible positions, and it's better to find that out early.
The Same Setup on Valkey or Redis
If you're weighing Hazelcast against a Valkey or Redis cache, the Spring-level picture is close enough that the comparison is mostly about operations. Redisson is a Java client that provides the same distributed-object model on top of a Valkey or Redis server, so the cache manager swap is a configuration change rather than a rewrite.
// Hazelcast
@Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new HazelcastCacheManager(hazelcastInstance);
}
// Redisson — annotations and call sites unchanged
@Bean
public CacheManager cacheManager(RedissonClient redisson) {
return new RedissonSpringCacheManager(redisson);
}
Add the redisson-spring-boot-starter and your @Cacheable, @CachePut and @CacheEvict annotations carry over untouched. The Spring Cache documentation covers per-cache TTL configuration.
The two Hazelcast features covered above map directly:
- Near cache →
RLocalCachedMap. Keeps hot entries in the application process and invalidates them cluster-wide over pub/sub — the same trade-off as Hazelcast's near cache, and available without an Enterprise license. See client-side caching for how the invalidation works. - Per-entry TTL →
RMapCache. A per-entry TTL that doesn't require dropping out of the abstraction, plusRMapCacheNativefor server-side hash-field expiry on Valkey 9.0+ and Redis 7.4+.
// per-entry TTL without leaving the map API
RMapCache<Long, Product> products = redisson.getMapCache("products");
products.put(1L, product, 10, TimeUnit.MINUTES);
// near-cache equivalent
LocalCachedMapOptions<Long, Product> options = LocalCachedMapOptions.<Long, Product>defaults()
.cacheSize(1000)
.syncStrategy(LocalCachedMapOptions.SyncStrategy.INVALIDATE);
RLocalCachedMap<Long, Product> localMap = redisson.getLocalCachedMap("products", options);
Two differences that usually decide it. TLS and authentication are included at no licence cost, because they're the Valkey or Redis server's responsibility rather than a client feature. And there's no cluster to operate — Redisson connects to a server you very likely already run, or to a managed service such as ElastiCache, MemoryDB, Azure Cache, or Memorystore. The topology question that opens this guide simply doesn't arise.
The full picture is in the Spring Boot caching guide for Valkey and Redis, and everything above is in the Apache-2.0 community edition; Redisson PRO adds data partitioning, additional local-cache implementations, and enterprise support.
When to Keep Hazelcast
Hazelcast does things a client to a data server doesn't. Entry processors run your code on the member owning a key, avoiding get-modify-put round trips on hot entries. Distributed SQL queries maps and streams. Embedded mode puts data in the same JVM as your code, which no client can match on latency.
If you rely on those, keep Hazelcast. If you're using it as a distributed cache with @Cacheable — which describes a large share of Spring Boot deployments — you're running a clustered platform for a fraction of what it does. For the full picture beyond caching, including the object model, locks, and the architectural trade-offs, see migrating from Hazelcast to Valkey or Redis.
Frequently Asked Questions
What Is the Difference Between Redis and Hazelcast Cache?
Architecture, mainly. Hazelcast is a peer-to-peer cluster of JVM members that can run embedded inside your application; Redis and Valkey are servers you run separately and connect to as a client. Hazelcast can therefore hold data in the same process as your code and run computation on it, which Redis can't. Redis and Valkey are usually simpler to operate, are frequently already present in a stack, and are available as managed services from every major cloud. For the wider category, see our comparison of in-memory data grids.
Does Hazelcast Work With @Cacheable?
Yes. Declare a HazelcastCacheManager bean and add @EnableCaching, and each named cache becomes a Hazelcast IMap. Per-entry TTL is the one thing the annotation model can't express — for that you use IMap directly.
Is Hazelcast Caching Free for Production Use?
Community Edition is free under the Apache 2.0 and Hazelcast Community licences, and covers distributed caching, near cache, and map-level TTL. TLS, authentication, encryption, the High-Density Memory Store, WAN replication, and persistence require an Enterprise licence.
Should I Use Embedded or Client/Server With Spring Boot?
Client/server for most applications. Embedded gives the lowest read latency and is the mode Spring Boot falls back to, but it makes your application instances cluster members — so deploys cause rebalancing, and application memory and cache memory compete. Embedded earns its place when in-process data access is a hard latency requirement.