Ehcache to Redisson: Moving to a Distributed Java Cache

Published on
July 8, 2026

Most Java applications that need caching start with Ehcache — it's the most widely used JVM cache and it's dead simple for a single process. But there's a distinction that decides what "migrating" even means here, so it's worth stating first: Ehcache is a local, in-process cache library, not a cache server. It lives inside your JVM (on-heap, with off-heap and disk tiers), and going distributed means standing up a separate Terracotta Server cluster. Redisson is a distributed Java cache backed by Redis or Valkey — a server you very likely already run.

So moving from Ehcache to Redisson is less "swap one cache library for another" and more "move from local caching to a shared, distributed cache." The reason teams do it is almost always the same: they run more than one instance of their app and need the cache to be consistent across all of them, survive restarts, and scale past a single JVM — without operating a Terracotta tier.

There are two ways to bring Redisson in, and this guide covers both:

  • Add Redisson under your existing cache API. If you reach Ehcache through JCache (JSR-107) or as a Hibernate second-level cache, you swap the provider and keep your code. Low-friction.
  • Move to Redisson's native objects — for a truly distributed cache with a client-side near cache. This is where the real gains live.

What Ehcache Is (and Isn't)

Ehcache 3 gives you a type-safe CacheManager / Cache<K,V> API, tiered storage (on-heap → off-heap → disk), full JSR-107 (JCache) compliance, and Spring Cache and Hibernate integration through that JCache support. It's Apache 2.0, and for a single JVM it's excellent — reads never leave the process and there's no external infrastructure to run.

What it isn't is a distributed cache out of the box. Ehcache is local-first. To share a cache across nodes you add the ehcache-clustered module and run a Terracotta Server — a separate tier to deploy, operate, and (for its enterprise capabilities) license. That's the friction that sends teams looking, and it sets an honest expectation for this guide: if your app is a single process and a local cache is all you'll ever need, Ehcache is lighter and you may not need to move at all. The rest of this guide is for when you've outgrown that.

Signs You've Outgrown Ehcache

  • You run more than one instance of your app and hit cache inconsistency — each JVM holds its own Ehcache, so a value updated on one node stays stale on the others until its TTL lapses.
  • You need the cache to survive restarts and deploys. In-process caches start cold every time, so a rolling deploy means a thundering herd on your database.
  • You're eyeing Terracotta Server to get clustering — i.e., about to take on a whole distributed tier just to share a cache.
  • You want the cache and the rest of your distributed toolkit — locks, counters, queues — in one place, rather than a cache library plus a separate coordination story.

None of these are Ehcache doing something wrong; they're the point where an in-process cache stops fitting a distributed system.

Ehcache vs. Redisson at a Glance

DimensionRedissonEhcache
TopologyDistributed, server-backed (Redis/Valkey), shared by all nodesLocal / in-process (heap, off-heap, disk); clustered only via Terracotta Server
Shared cache across nodesNativeRequires a Terracotta Server tier
Near / local cacheRLocalCachedMap — client near-cache with pub/sub invalidationIt is a local cache; cross-node sync needs Terracotta
Survives app restartYes (state lives in Redis/Valkey)No (in-JVM tiers are per-process)
JCache (JSR-107)Yes (TCK-certified)Yes
Hibernate L2Yes (region factory or JCache adapter)Yes (via JCache)
Spring CacheYesYes
Distributed locks / objects / services60+ objects: locks, counters, queues, and moreNone (cache only)
Infrastructure to operateRedis/Valkey (likely already running)None for local; Terracotta Server for clustered
LicenseApache 2.0 (community)Apache 2.0

What Redisson Adds

A Shared Cache, Not a Per-JVM One

Every Redisson cache object lives in Redis or Valkey, so all your app instances read and write the same cache. Update a value on one node and every node sees it — no waiting for a per-JVM copy to expire. The cache also outlives your app: restart or redeploy and it's still warm, so you don't stampede the database on every boot. Getting the equivalent from Ehcache means running a Terracotta Server cluster; with Redisson it's the default behavior against the Redis or Valkey you already operate. This is the main reason to move, and it's why a distributed Java cache is a different tool than an in-process one.

A Near Cache for Local-Speed Reads

Ehcache's genuine advantage is that local reads never touch the network. Redisson answers that with RLocalCachedMap — a near cache that keeps hot entries in the application's own memory for local-speed reads, while a pub/sub channel invalidates those local copies across every instance when the shared value changes. The docs cite reads "up to 45x faster" than a normal remote map (a vendor figure — treat it as such). You get Ehcache-like read latency and cluster-wide consistency at once, and it's in the community edition.

Standard Cache APIs, So the Migration Is Mostly Configuration

Redisson implements the same standards Ehcache does: JCache (JSR-107) — TCK-certified — and the Hibernate second-level cache, plus Spring Cache. If you reach Ehcache through any of those, you keep your application code and swap the provider.

The Rest of the Distributed Toolkit

Because Redisson is a full data platform, the same client that serves your cache also gives you distributed locks (RLock with a watchdog), atomic counters, queues, rate limiters, and 50+ other objects — capabilities Ehcache doesn't have, because it's a cache and nothing more.

Compatibility

Redisson connects to the infrastructure your team most likely already runs: Redis 3.0+ and Valkey 7.2.5+, via the redis://, rediss://, valkey://, and valkeys:// schemes, including managed services such as AWS ElastiCache and MemoryDB, Azure Cache, and Google Cloud Memorystore. Unlike moving to a clustered Ehcache — which introduces a Terracotta Server — adopting Redisson doesn't add a new kind of server; it points at a Redis or Valkey endpoint.

Two Ways to Bring in Redisson

Which Path?

Reach for Path A (swap the provider) when you already use Ehcache through JCache, Hibernate L2, or Spring Cache and you want a distributed cache without touching call sites — you change a dependency and configuration, not code. Reach for Path B (native objects) when you want the near cache, per-entry TTL, and the wider object model, and you're happy to program against Redisson directly. They combine: swap the provider now, and adopt RLocalCachedMap where you need local-speed reads.

Path A — Keep Your Cache API, Swap the Provider

JCache (JSR-107)

Both Ehcache and Redisson are JCache providers, so your javax.cache code doesn't change — only the provider on the classpath and its config file do.

Ehcache dependencies:

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.11.1</version>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.0</version>
</dependency>

Redisson dependencies:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>4.6.1</version>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.0</version>
</dependency>

The application code is identical — that's the entire point of JSR-107:

// Same for both providers
CachingProvider provider = Caching.getCachingProvider();
CacheManager cacheManager = provider.getCacheManager();
MutableConfiguration<Long, String> config =
        new MutableConfiguration<Long, String>().setTypes(Long.class, String.class);
Cache<Long, String> cache = cacheManager.createCache("products", config);
cache.put(1L, "Widget");
String value = cache.get(1L);

The only difference is which provider is selected. With a single provider on the classpath, Caching.getCachingProvider() finds it automatically; with both present, name it explicitly:

// Ehcache
CachingProvider ehcache =
        Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");

// Redisson
CachingProvider redisson =
        Caching.getCachingProvider("org.redisson.jcache.JCachingProvider");

Where Ehcache reads an ehcache.xml, Redisson reads a redisson-jcache.yaml (or JSON) on the classpath describing your Redis/Valkey connection. Your cache calls stay put.

Hibernate Second-Level Cache

Since Hibernate 6, the standard way to plug in an L2 cache is the JCache adapter, so an Ehcache L2 configuration looks like this:

<!-- Ehcache as Hibernate L2, via the JCache adapter -->
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.region.factory_class"
          value="org.hibernate.cache.jcache.internal.JCacheRegionFactory"/>
<property name="hibernate.javax.cache.provider"
          value="org.ehcache.jsr107.EhcacheCachingProvider"/>
<property name="hibernate.javax.cache.uri" value="ehcache.xml"/>

Redisson ships a dedicated region factory, which is the richer swap — per-region TTL and eviction, plus optional fallback to the database if Redis or Valkey is unavailable:

<!-- Redisson as Hibernate L2 -->
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.use_query_cache" value="true"/>
<property name="hibernate.cache.region.factory_class"
          value="org.redisson.hibernate.RedissonRegionFactory"/>
<property name="hibernate.cache.redisson.config" value="redisson.yaml"/>
<property name="hibernate.cache.redisson.fallback" value="true"/>

with:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-hibernate-6</artifactId>
    <version>4.6.1</version>
</dependency>

All four Hibernate concurrency strategies (READ_ONLY, NONSTRICT_READ_WRITE, READ_WRITE, TRANSACTIONAL) are supported, and your entity @Cache annotations don't change. The artifact suffix tracks your Hibernate line — redisson-hibernate-6 for Hibernate 6.x, redisson-hibernate-7 for Hibernate 7.x, and redisson-hibernate-53 for 5.3.

Spring Cache

If you cache method results with @Cacheable, register Redisson's Spring Cache manager (RedissonSpringCacheManager) in place of the JCache/Ehcache one, and your annotations are unchanged — now backed by Redis or Valkey. The Spring Boot Starter module also adds a starter that auto-configures the client.

Path B — Native Objects

Programming against Redisson directly is where the distributed cache, per-entry TTL, and near cache come in. First, a client (this replaces the Ehcache CacheManager):

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

Ehcache's Cache<K,V> then maps onto a small family of Redisson objects, each implementing a familiar Java interface.

Basic cache → RMap:

// Ehcache
Cache<Long, String> cache = cacheManager.getCache("products", Long.class, String.class);
cache.put(1L, "Widget");
String value = cache.get(1L);
cache.remove(1L);

// Redisson — RMap implements java.util.concurrent.ConcurrentMap
RMap<Long, String> map = redisson.getMap("products");
map.put(1L, "Widget");
String value = map.get(1L);
map.remove(1L);

Per-entry TTL and idle expiry → RMapCache / RMapCacheNative:

Ehcache defines expiry on the cache; Redisson's RMapCache sets it per entry.

// Ehcache — expiry defined in the cache configuration
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
        ResourcePoolsBuilder.heap(1000))
    .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)));

// Redisson — per-entry TTL and max-idle
RMapCache<Long, String> cache = redisson.getMapCache("products");
cache.put(1L, "Widget", 10, TimeUnit.MINUTES);                       // TTL
cache.put(2L, "Gadget", 10, TimeUnit.MINUTES, 5, TimeUnit.MINUTES);  // TTL + max-idle

RMapCache evicts with a client-side scheduled task; for server-side expiry use RMapCacheNative, which relies on native hash-field TTL (Valkey 9.0+ / Redis 7.4+).

Near cache → RLocalCachedMap:

For the local-read speed Ehcache gave you, plus cross-node invalidation:

LocalCachedMapOptions<Long, String> options = LocalCachedMapOptions.<Long, String>defaults()
    .cacheSize(1000)
    .evictionPolicy(LocalCachedMapOptions.EvictionPolicy.LRU)
    .timeToLive(Duration.ofMinutes(10))
    .syncStrategy(LocalCachedMapOptions.SyncStrategy.INVALIDATE);

RLocalCachedMap<Long, String> map = redisson.getLocalCachedMap("products", options);
map.put(1L, "Widget");
String value = map.get(1L);   // served from local memory; invalidated cluster-wide on change

RLocalCachedMap extends RMap, so it's a drop-in for the basic case with a local tier added. The INVALIDATE sync strategy clears a node's local copy over pub/sub whenever the shared entry changes.

And the rest: the same redisson client also exposes RLock, RAtomicLong, RQueue, and the other 60+ objects, so coordination lives alongside the cache instead of in a separate library.

Technical Considerations and Implementation Pitfalls

  • Serialization / codec, not storeByValue. JCache defaults to storeByValue (copies via serialization); Redisson uses a configurable codec (Kryo/JSON/etc.). Cached types must be serializable under the codec you pick, and if you read the same keys through two different Redisson objects, keep their codecs consistent.
  • Client-side vs server-side eviction. RMapCache enforces per-entry TTL with a client-side scheduled task, which adds background calls. For native server-side hash-field expiry, use RMapCacheNative (Valkey 9.0+ / Redis 7.4+).
  • Near-cache consistency. RLocalCachedMap trades a small staleness window for local-read speed; the INVALIDATE strategy clears local copies on change, but very write-heavy keys are usually better served without a near cache.
  • A single cache object lives on one node. By default a Redisson object sits on one master, whereas spreading one large cache across cluster nodes — data partitioning — is a Redisson PRO feature.
  • Don't forget the query cache. If you relied on Hibernate's query cache with Ehcache, enable hibernate.cache.use_query_cache=true on the Redisson side too.

Ehcache to Redisson Migration: Frequently Asked Questions

Does Redisson Replace Ehcache?

It can. Either swap the provider under your existing JCache, Hibernate, or Spring cache code, or move to Redisson's native objects. The difference from Ehcache is that the cache becomes distributed and shared across all your instances instead of living in each JVM.

Can I Keep My JCache (javax.cache) Code?

Yes — both are JSR-107 providers, so your CacheManager/Cache code is unchanged. You swap the provider on the classpath and its config file.

Does Redisson Work as a Hibernate Second-Level Cache?

Yes, via redisson-hibernate-6 (RedissonRegionFactory) or the JCache adapter, with all four concurrency strategies and optional fallback to the database.

Do I Need to Run a Separate Server Like Terracotta?

No. Redisson connects to Redis or Valkey, which you most likely already operate — there's no extra caching tier to stand up.

Does Redisson Give Me a Local Cache Like Ehcache?

Yes — RLocalCachedMap keeps hot entries in the application's memory for local-speed reads and invalidates them across the cluster when the shared value changes.

Does Redisson Work With Valkey?

Yes, with Valkey 7.2.5+ (and Redis 3.0+), the same endpoints you already target.