Spring Data Redis to Redisson: When to Add It and When to Migrate

Published on
July 7, 2026

If you build on Spring Boot, you almost certainly talk to Redis or Valkey through Spring Data RedisRedisTemplate, @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 RedisTemplate and @Cacheable code exactly as-is and swap the underlying connection factory to RedissonConnectionFactory. 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 RedisTemplate to Redisson's native objects. Use RMap, 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. RedissonConnectionFactory and LettuceConnectionFactory both implement RedisConnectionFactory and 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 — RedissonAutoConfigurationV2 on Spring Boot 2.7+ (or the bare RedissonAutoConfiguration on Spring Boot 2.6 and earlier).
  • Serialization and key views differ. RedisTemplate uses configurable serializers (often StringRedisSerializer / JDK / JSON), while Redisson objects use their own codec (JSON/Kryo/etc.). A value written through RedisTemplate and read back as an RMap/RBucket of 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 RedisTemplate configuration maps one-to-one. Exotic serializer setups, custom RedisTemplate beans, 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 RMapCache enforces per-entry TTL with a client-side scheduled task, which adds background calls. For server-side TTL, use RMapCacheNative, 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.