Spring Data Redis to Redisson: When to Add It and When to Migrate
If you build on Spring Boot, you almost certainly talk to Redis or Valkey through Spring Data Redis — RedisTemplate, @Cacheable, and repository support. It's the idiomatic, well-supported way to use Redis in Spring, and for a lot of applications it's all you need.
But there's an important thing to get straight before you "migrate," because it changes what the move even means: Spring Data Redis is not a client — it's an abstraction layer that sits on top of a driver (Lettuce by default, or Jedis). Redisson relates to it in two different ways, and a clean adoption picks the right one:
-
Add Redisson under Spring Data Redis. Keep your
RedisTemplateand@Cacheablecode exactly as-is and swap the underlying connection factory toRedissonConnectionFactory. This is augmentation, not migration — the lowest-risk way to get Redisson's locks and objects alongside the Spring code you already have. -
Move off
RedisTemplateto Redisson's native objects. UseRMap,RLock,RScoredSortedSet, and the rest directly. This is the real migration, and where the productivity and feature gains live.
This guide covers both, and when each makes sense.
First, What Spring Data Redis Is (and Isn't)
Spring Data Redis gives you RedisTemplate and its opsForValue() / opsForHash() / opsForList() / opsForZSet() operation views, the @Cacheable cache abstraction, repository support, and a RedisConnectionFactory that wraps an actual driver. It does not ship its own protocol client — it delegates to Lettuce (the Spring Boot default) or Jedis.
That matters because it sets honest expectations: Spring Data Redis is excellent, standard Spring, and if your app just reads and writes values through RedisTemplate and caches a few method results with @Cacheable, you may not need to leave it at all. The rest of this guide is for teams who've hit its ceiling — usually because they need a distributed lock, want to stop juggling string-keyed templates, or need capabilities the abstraction simply doesn't model.
What Redisson Adds on Top of Spring Data Redis
Distributed Locks — The Clearest Gap
Spring Data Redis has no distributed lock abstraction.
In Redisson it's three lines:
RLock lock = redisson.getLock("order:1234");
lock.lock();
try {
// critical section, safe across every node and JVM
} finally {
lock.unlock();
}
A watchdog automatically extends the lock's lease while the holder is alive (default 30 seconds, set via Config.lockWatchdogTimeout) when you call lock() with no explicit lease, so a long task won't lose its lock and a crashed holder won't keep it forever. If you'd rather cap the hold time yourself, tryLock(waitTime, leaseTime, unit) acquires within a bound and auto-releases after the lease. Redisson also provides fair locks, multi-locks, read/write locks, fenced locks, spin locks, semaphores, permit-expirable semaphores, and countdown latches — each with async, reactive, and RxJava variants. This is the single most common reason teams move beyond RedisTemplate.
Typed Objects Instead of opsForX() Templates
With Spring Data Redis you work through a single RedisTemplate and choose an operation view for each data type — opsForHash(), opsForList(), and so on — passing the key on every call. With Redisson you get a typed object that is the data structure and implements a familiar Java interface:
// Spring Data Redis
redisTemplate.opsForHash().put("user:1", "name", "Ada");
Object stored = redisTemplate.opsForHash().get("user:1", "name");
// Redisson — RMap implements java.util.concurrent.ConcurrentMap
RMap<String, String> user = redisson.getMap("user:1");
user.put("name", "Ada");
String name = user.get("name");
RMap implements ConcurrentMap, RList implements List, RSet implements Set, and RQueue/RDeque/RBlockingQueue implement the queue interfaces — across 60+ objects. Your domain code stops passing string keys into operation views and starts holding collections.
Spring Cache and Spring Session, Upgraded
Redisson implements the same Spring abstractions you already use, with options the defaults don't have. Its Spring Cache managers add per-cache TTL and maxIdleTime, and — in Redisson PRO — local-cached managers that keep a near-cache on the client and report reads "up to 45x faster" than a standard implementation (a vendor benchmark figure; treat it as such). For Spring Session, Redisson plugs in by providing RedissonConnectionFactory, so @EnableRedisHttpSession works against Redisson with no change to your session code.
Managed Near-Cache, JCache, and Distributed Services
Beyond Spring's abstractions, Redisson adds a managed near-cache (RLocalCachedMap object) with configurable invalidation and eviction, a JCache (JSR-107) provider, and distributed services with no Spring Data Redis counterpart: a remote-invocation (RPC) service, a Live Object service, a distributed executor and scheduler, and MapReduce. In Redisson PRO these extend to enterprise-grade reliable messaging — a Reliable Queue and Reliable Pub/Sub that back a TCK-passing JMS provider.
Compatibility
This is parity, not a differentiator: Redisson talks to the same servers Spring Data Redis does. It's officially a "Valkey & Redis Java client," compatible with Redis 3.0+ and Valkey 7.2.5+, and accepts the redis://, rediss://, valkey://, and valkeys:// URL schemes, including managed services such as AWS ElastiCache and MemoryDB, Azure Cache, and Google Cloud Memorystore. Whether Redisson runs as your Spring Data Redis driver or replaces RedisTemplate, you keep your existing deployment.
Two Ways to Bring in Redisson
Which Path?
Reach for Path A (Redisson as the driver) when you have a large existing RedisTemplate/@Cacheable codebase you don't want to rewrite, but you need one or two things Spring Data Redis can't give you — most often a distributed lock. You change configuration, not call sites.
Reach for Path B (native objects) when the value you want is the object model and services — typed collections, the full lock family, distributed executors, live objects — and you'd rather write against Redisson directly. You can do this gradually, alongside RedisTemplate.
The two aren't exclusive: a common pattern is Path A everywhere plus Path B for the new code that needs locks or services.
Path A — Keep Spring Data Redis, Swap the Factory
Add the Redisson Spring Boot starter. It auto-configures a RedissonClient and registers a RedissonConnectionFactory, which implements Spring Data Redis's RedisConnectionFactory, so your RedisTemplate and @Cacheable code keeps working unchanged — now backed by Redisson.
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
// Your existing Spring Data Redis code is untouched…
@Autowired RedisTemplate<String, String> redisTemplate;
redisTemplate.opsForValue().set("user:1", "Ada");
// …and you can inject RedissonClient alongside it for the things RedisTemplate can't do:
@Autowired RedissonClient redisson;
RLock lock = redisson.getLock("order:1234");
Because RedissonConnectionFactory and LettuceConnectionFactory both implement RedisConnectionFactory, you run one or the other — not both. With the starter on the classpath, exclude Lettuce so there's a single factory:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
That's the whole of Path A: configuration, not code. You now have RedissonClient available everywhere for locks, objects, and services, while every existing RedisTemplate call site stays exactly as written.
Path B — Move to Native Objects
When you want the object model itself, translate RedisTemplate operation views into Redisson objects. In Spring Data Redis you pick an opsForX() view and pass the key each call; in Redisson you obtain the object once and call methods on it.
Strings / values:
// Spring Data Redis
redisTemplate.opsForValue().set("user:1", "Ada");
String name = redisTemplate.opsForValue().get("user:1");
// Redisson
RBucket<String> bucket = redisson.getBucket("user:1");
bucket.set("Ada");
String name = bucket.get();
Set with TTL or only-if-absent (SETEX / SETNX):
// Spring Data Redis
redisTemplate.opsForValue().set("k", "v", Duration.ofSeconds(60));
redisTemplate.opsForValue().setIfAbsent("k", "v");
// Redisson
bucket.set("v", Duration.ofSeconds(60)); // SETEX
boolean created = bucket.trySet("v"); // SETNX
Counters:
// Spring Data Redis
redisTemplate.opsForValue().increment("visits");
redisTemplate.opsForValue().increment("visits", 5);
// Redisson
RAtomicLong visits = redisson.getAtomicLong("visits");
visits.incrementAndGet();
visits.addAndGet(5);
Key lifecycle and expiration:
// Spring Data Redis
redisTemplate.expire("user:1", Duration.ofSeconds(60));
redisTemplate.delete("user:1");
Boolean exists = redisTemplate.hasKey("user:1");
// Redisson — per-object methods, plus RKeys for bulk or pattern operations
RBucket<String> b = redisson.getBucket("user:1");
b.expire(Duration.ofSeconds(60));
b.delete();
boolean present = b.isExists();
RKeys keys = redisson.getKeys();
Iterable<String> matched = keys.getKeysByPattern("user:*");
Lists, sets, sorted sets:
// Spring Data Redis
redisTemplate.opsForList().rightPush("tasks", "first");
redisTemplate.opsForSet().add("tags", "java");
redisTemplate.opsForZSet().add("leaderboard", "alice", 100.0);
// Redisson — each implements the matching java.util type
RList<String> list = redisson.getList("tasks");
list.add("first");
RSet<String> set = redisson.getSet("tags");
set.add("java");
RScoredSortedSet<String> board = redisson.getScoredSortedSet("leaderboard");
board.add(100.0, "alice");
Transactions:
// Spring Data Redis — SessionCallback wraps MULTI/EXEC on one connection
redisTemplate.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) {
operations.multi();
operations.opsForValue().set("a", "1");
operations.opsForValue().set("b", "2");
return operations.exec();
}
});
// Redisson — RBatch pipelines; RTransaction adds ACID semantics
RBatch batch = redisson.createBatch();
batch.getBucket("a").setAsync("1");
batch.getBucket("b").setAsync("2");
batch.execute();
Pub/Sub:
// Spring Data Redis — container + listener to subscribe; convertAndSend to publish
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(
(message, pattern) -> handle(new String(message.getBody())),
new ChannelTopic("news"));
redisTemplate.convertAndSend("news", "hello");
// Redisson
RTopic topic = redisson.getTopic("news");
topic.addListener(String.class, (channel, msg) -> handle(msg));
topic.publish("hello");
For the full set, Redisson's command-to-object mapping reference maps every Redis and Valkey command to its equivalent object and method across the sync, reactive, and RxJava APIs. As that page notes, distributed features like locks, rate limiters, near-cache, and reliable messaging aren't listed there at all — because they correspond to no single command, which is precisely the layer this migration buys you.
Note that you don't have to choose all-or-nothing per call site: with Path A's connection factory in place, RedisTemplate and Redisson objects operate against the same server, so you can migrate hot paths to native objects while leaving the rest on RedisTemplate.
Technical Considerations and Implementation Pitfalls
-
One connection factory, not two.
RedissonConnectionFactoryandLettuceConnectionFactoryboth implementRedisConnectionFactoryand can't both be the active bean — exclude Lettuce as shown. To opt out of Redisson's auto-configuration entirely, exclude its auto-configuration class —RedissonAutoConfigurationV2on Spring Boot 2.7+ (or the bareRedissonAutoConfigurationon Spring Boot 2.6 and earlier). -
Serialization and key views differ.
RedisTemplateuses configurable serializers (oftenStringRedisSerializer/ JDK / JSON), while Redisson objects use their own codec (JSON/Kryo/etc.). A value written throughRedisTemplateand read back as anRMap/RBucketof the same key may not deserialize as expected unless the codecs match — so when you run Path A and Path B against the same keys, align serialization or keep their keyspaces separate. -
Not every
RedisTemplateconfiguration maps one-to-one. Exotic serializer setups, customRedisTemplatebeans, and some repository features don't have a direct Redisson-object equivalent; the connection-factory swap (Path A) preserves them, native objects (Path B) don't. -
Eviction model for caches. Redisson's
RMapCacheenforces per-entry TTL with a client-side scheduled task, which adds background calls. For server-side TTL, useRMapCacheNative, which relies on native hash-field expiration (available on Valkey 9.0+ / Redis 7.4+). - Single-object placement in a cluster. A single Redisson object lives on one master node by default. Spreading one large structure across cluster nodes — data partitioning — is a Redisson PRO feature, not part of the community edition.