Distributed Caching in Java with Valkey and Redis
When an application runs on a single machine, caching is simple: keep frequently used data in memory and read it back in microseconds instead of paying for a database round trip every time. The problem is that almost no serious Java application runs on a single machine anymore. It runs as a fleet of instances behind a load balancer, scaling horizontally across containers, pods, and availability zones. And the stakes are commercial as much as technical — slow responses cost conversions and retention, and every avoidable database hit adds infrastructure load you pay for.
The moment you have more than one instance, a plain in-process cache becomes a liability. Each node holds its own private copy of the data, those copies drift apart, and a user who updates a record on one node reads stale data from another. Distributed caching is the technique that solves this: instead of every node guessing on its own, the cache is shared across the cluster so all instances see a consistent view, while still serving reads at memory speed.
This article walks through how to implement distributed caching in Java on top of Valkey and Redis using Redisson, covering distributed maps, near caching, Spring Cache, JCache and — the part most guides skip — how to keep all of it consistent.
What Is Distributed Caching, and When Do You Need It?
Distributed caching is a caching technique in which the cache is spread across multiple servers rather than confined to a single process or machine. Frequently accessed data lives in a fast in-memory tier shared by every application instance, so reads avoid the slower primary database and the cache scales as you add nodes.
You need it once any of the following is true:
- You run more than one instance of your service and they must agree on cached data.
- A local, per-instance cache would go stale because writes happen on other nodes.
- Your cache no longer fits comfortably in the heap of a single machine.
- You want cached data to survive an individual instance restart.
Valkey and Redis are the natural backing store for this tier. Both are open-source, in-memory data structure stores that read and write from memory rather than disk, which makes them dramatically faster than a traditional database for hot data. Valkey is the community-driven fork of Redis, and Redisson speaks to both with the same API — so everything below works regardless of which one you deploy.
Distributed Caching Architecture With Valkey & Redis
There are three layers worth distinguishing, because the right Redisson construct depends on which one you are building:
- Server-side cache. Data lives only in Valkey/Redis. Every read is a network round trip to the cache server. Simple, always consistent, but you pay network latency on every lookup.
- Client-side (local) cache. A subset of entries is also kept in the application's own memory. Reads that hit the local copy never touch the network at all. This is the fastest option but introduces the cache invalidation problem: when the shared data changes, every local copy must be told to refresh or evict.
- Near cache. The combination of the two. Each instance checks its local cache first, falls back to the shared Valkey/Redis cache on a cache miss, and only then hits the database. This gives you single-node read speed with cluster-wide consistency.
Redisson provides all three through familiar Java interfaces, so you write ordinary Map-style code and the distribution happens underneath.
How to Implement a Redis or Valkey Cache in Java With Redisson
Redisson is a Valkey and Redis Java client that reimplements many standard Java structures — Map, Set, List, Queue, locks, and more — as distributed objects backed by the cache. For caching specifically, the most important of these is the distributed map, and the examples below show each option in Java.
Distributed Maps With RMapCache
The simplest distributed cache you can build on Redis or Valkey is a map. Redisson's RMapCache is a Redis hash-backed implementation of Java's Map with first-class cache semantics: every entry can carry its own time-to-live (TTL) and maximum idle time, after which it is evicted automatically. This per-entry expiration is enforced on the client side — Redisson runs a scheduled eviction task that removes expired entries using a Lua script, rather than the Valkey/Redis server expiring them itself.
RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap");
// store an entry that expires 10 minutes after writing,
// or 10 seconds after it was last accessed — whichever comes first
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES, 10, TimeUnit.SECONDS);
SomeObject value = map.get("key1");
For write-heavy paths, every Redisson map also exposes fast* variants — fastPut, fastPutIfAbsent, and fastRemove — that apply the change without returning the previous value. Skipping that return value avoids an extra read and deserialization round trip, so these methods are measurably faster than their standard counterparts whenever you don't actually need the old value back.
Native Expiration With RMapCacheNative
Because that eviction task runs on the client side, expiration depends on your application being alive to run it — if every instance is down, entries can outlive their TTL until a Redisson client reconnects and resumes eviction.
RMapCacheNative removes that dependency. It delegates per-entry expiration to the server using native hash-field TTL, so entries are cleaned up by Valkey/Redis itself with no Redisson-side eviction task. That makes it lighter and more dependable for TTL-critical data such as session or rate-limit state. It requires native hash-field expiration support — Redis 7.4 or later, and Valkey 9.0+.
RMapCacheNative<String, SomeObject> map = redisson.getMapCacheNative("anyMap");
// expiration is handled natively by Valkey/Redis, not by a Redisson task
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES);
SomeObject value = map.get("key1");
On a recent Valkey/Redis version, prefer RMapCacheNative for straightforward TTL-based distributed caching. Reach for the classic RMapCache when you need max-idle eviction or have to support older servers.
Near Caching With RLocalCachedMap
When a cache is read far more often than it is written — which describes most caches — paying for a network round trip on every read is wasteful. RLocalCachedMap keeps a copy of frequently used entries in the application's own memory, so repeat reads are served locally. Reads against a warm local cache can be many times faster than going to the cache server, because they never leave the JVM.
LocalCachedMapOptions<String, SomeObject> options = LocalCachedMapOptions.<String, SomeObject>name("anyMap")
.evictionPolicy(EvictionPolicy.LRU)
.cacheSize(10_000)
.syncStrategy(SyncStrategy.INVALIDATE)
.timeToLive(Duration.ofMinutes(10))
.maxIdle(Duration.ofSeconds(10));
RLocalCachedMap<String, SomeObject> map = redisson.getLocalCachedMap(options);
map.put("key1", new SomeObject());
SomeObject value = map.get("key1"); // served from local memory after the first read
The important setting here is syncStrategy, which controls how Redisson keeps the local copies consistent across the cluster:
-
INVALIDATE— when an entry changes, other nodes drop their local copy and reload it on next access. The safe default. -
UPDATE— the new value is pushed to other nodes proactively. -
NONE— no synchronization; only use this for data that never changes.
The evictionPolicy controls what the local cache discards when it reaches cacheSize — for example an LRU (least recently used) policy keeps the hottest entries and drops the coldest. This is the part of distributed caching that is genuinely hard to get right by hand, and it is exactly what Redisson manages for you.
Scaling the Cache With Data Partitioning
A single Valkey/Redis instance has finite memory, and a single local cache lives inside a single heap. For large datasets you need to spread the cache across a cluster using sharding, so that no one node has to hold everything.
Redisson PRO adds data partitioning for local cached maps, automatically distributing entries across multiple cluster shards while preserving the same RLocalCachedMap programming model. Your code stays the same; the cache scales horizontally with your cluster instead of being bounded by a single node, which expands available memory, increases read/write throughput, and reduces the CPU and network load on your Valkey/Redis deployment.
Spring Cache and Spring Boot
If your application uses Spring or Spring Boot, you usually do not want to call a cache API directly at all — you want to annotate methods with @Cacheable and let the framework handle it. Redisson plugs into Spring Cache through a CacheManager, so a Redis or Valkey cache becomes the engine behind those annotations — the Redisson way to configure a Spring Boot Redis cache.
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean(destroyMethod = "shutdown")
RedissonClient redisson() {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7001", "redis://127.0.0.1:7004");
return Redisson.create(config);
}
@Bean
CacheManager cacheManager(RedissonClient redissonClient) {
Map<String, CacheConfig> config = new HashMap<>();
// "catalog" cache: TTL 30 min, max idle 15 min
config.put("catalog", new CacheConfig(30 * 60 * 1000, 15 * 60 * 1000));
return new RedissonSpringCacheManager(redissonClient, config);
}
}
The two CacheConfig arguments are the TTL and the max idle time, both expressed in milliseconds; a value of 0 (or leaving them unset) keeps entries indefinitely, deferring eviction to your own policy or to Valkey/Redis. Behind the annotations, @Cacheable implements the cache-aside pattern — for how cache-aside compares with read-through, write-through, and write-behind, see Caching Patterns in Java with Valkey & Redis.
For a near-cache version of the same thing, Redisson PRO provides RedissonSpringLocalCachedCacheManager, which keeps a local copy on each instance and invalidates it across the cluster automatically — the Spring Cache equivalent of RLocalCachedMap, with the same read/write speedups.
JCache (JSR-107)
JCache is the standard caching API for Java. Coding against it keeps your application portable across cache providers, and Redisson ships a fully compliant implementation backed by Valkey/Redis.
MutableConfiguration<String, String> config = new MutableConfiguration<>();
CacheManager manager = Caching.getCachingProvider().getCacheManager();
Cache<String, String> cache = manager.createCache("namedCache", config);
cache.put("key", "value");
String value = cache.get("key");
To control connection settings, point the caching provider at an external Redisson configuration file rather than relying on defaults — Caching.getCachingProvider().getCacheManager(redissonConfigUri, null) loads the cache from a redisson.yaml or redisson.json resource on the classpath or filesystem.
Redisson's JCache implementation passes the full JSR-107 Technology Compatibility Kit, so you get a standards-compliant distributed cache without locking yourself into a proprietary API. Redisson PRO extends JCache further with local caching, native and advanced entry eviction, and data partitioning across the cluster — the same up to 45x faster reads and 4x faster writes available to the other cache implementations.
Cache Invalidation and Consistency Strategies
The hard part of distributed caching is never the storing — it is the invalidating. A cache that returns stale data is worse than no cache at all. There are a few levers Redisson gives you to keep cached data correct.
Expiration. Every RMapCache entry can have a TTL and a max idle time. TTL evicts an entry a fixed time after it is written; max idle evicts it after a period with no access. Use TTL when data has a known freshness window and max idle to reclaim memory from entries nobody is reading. When expiration is business-critical and application uptime isn't guaranteed, prefer RMapCacheNative, which lets Valkey/Redis expire entries natively rather than relying on a Redisson-side task.
Eviction policies. When a local cache fills up, the eviction policy (LRU, LFU, soft, or weak references) decides what to discard so the cache never grows unbounded.
Cross-node synchronization. For near caches, the INVALIDATE and UPDATE sync strategies described earlier are what keep every instance's local copy consistent when data changes anywhere in the cluster. This is the mechanism that makes a local cache safe to use in a distributed system.
Read-through and write-through / write-behind. Rather than loading and saving the underlying database by hand, you can attach a MapLoader and a MapWriter to any Redisson map so the cache stays in sync with your system of record automatically: read-through fills the cache on a cache miss, while write-through and write-behind persist changes synchronously or in asynchronous batches.
Choosing an Approach
| Construct | Where data lives | Best for | Cross-node consistency |
|---|---|---|---|
RMapCache | Valkey/Redis only | General distributed cache, always-consistent reads | Inherent (single shared copy) |
RMapCacheNative | Valkey/Redis only | TTL-based caching with server-side expiration (Redis 7.4+) | Inherent (single shared copy) |
RLocalCachedMap | Local + Valkey/Redis | Read-heavy workloads needing lowest read latency | Sync strategy (INVALIDATE / UPDATE) |
| Clustered local cache (PRO) | Partitioned across cluster + local | Large caches that must scale horizontally | Sync strategy + data partitioning |
| Spring Cache manager | Valkey/Redis (local variant available) | Spring apps using @Cacheable | Inherent, or sync strategy for local variant |
| JCache | Valkey/Redis | Vendor-neutral, standards-based caching | Inherent |
A practical rule of thumb: start with RMapCache for a straightforward distributed cache, move to RLocalCachedMap once read latency on a hot path matters, and reach for PRO data partitioning once the dataset outgrows a single node.
Distributed Caching: The Bottom Line
Distributed caching is what lets a horizontally scaled Java application stay fast without sacrificing consistency. Valkey and Redis provide the fast shared in-memory tier, and Redisson turns that tier into ordinary Java objects — distributed maps, near caches, Spring Cache and JCache — while handling the genuinely difficult parts: eviction, expiration, and cross-node invalidation.
Where to go next: the Redisson documentation covers each caching construct in depth, the feature comparison shows exactly what Community Edition and PRO each include, and you can start a free trial to benchmark PRO's local caching and data partitioning against your own workload. For licensing on a production deployment, request pricing.
Beyond data caching, Redisson covers adjacent needs built on the same Valkey/Redis foundation — including distributed web session management for Tomcat and Spring Session, which is itself a form of distributed caching for session state.