Jedis to Redisson Migration Guide

Published on
July 1, 2026

Jedis is one of the longest-running, most straightforward Java clients within the Redis ecosystem. Part of the official Redis client family (along with Lettuce) maintained by Redis Ltd., Jedis can execute all the fundamental network calls to a Redis server. If your application only requires sending raw commands and awaiting synchronous replies, Jedis can get the job done. However, the architecture of modern enterprise applications can quickly outgrow the limitations of a client that only issues raw commands.

When infrastructure teams find themselves manually engineering a distributed lock using SET NX PX and Lua release scripts or attempting to coerce raw hash structures into behaving like native java.util.Map objects, Jedis is no longer a time saver. It becomes a bottleneck that requires you to maintain an untold amount of bespoke code. In addition, it's hard to imagine leveraging distributed objects, concurrency locks, and other higher-level services built upon Redis with a strictly command-level client like Jedis.

The Redisson Java client for Valkey/Redis and its commercial counterpart, Redisson PRO, offer developers so much more. Redisson translates Valkey and Redis features into standard Java objects. Even better, Redisson offers custom distributed objects for features not found in Valkey or Redis out of the box, including Reliable PubSub for guaranteed message delivery, a full JMS API, web session clusters, distributed caches, and much more. This makes Redisson the premier Valkey/Redis client for Java developers — and it's not difficult to switch from Jedis. Here is a detailed guide to migrating from Jedis to Redisson.

Signs Your Architecture Has Outgrown Jedis

Before considering switching from Jedis to something else, you should be sure you really need to. If you only need to send basic commands to, or simply retrieve data from Redis, Jedis might be all you need. But if your apps are moving beyond a monolithic architecture and scaling horizontally, you may find yourself coding and maintaining costly workarounds. Here are some signs that your app architecture has outgrown Jedis:

  • You have implemented distributed locks manually: You need distributed synchronizers or lock semantics, but Jedis lacks native abstractions for them, so you are custom-building them with manual SET NX PX commands paired with bespoke Lua release scripts.
  • You're mapping semantics: Your application logic calls for Map, List, Set, Queue, or Deque semantics, but you are currently issuing raw HSET, RPUSH, or ZADD commands and mapping the results back to your domain models.
  • You need advanced managed caching: Your architecture requires a fully managed near-cache implementation or a compliant JCache (JSR-107) provider, which exceeds the capabilities of the low-level, client-side caching primitives introduced in Jedis 5.2.0.
  • You need distributed services: Your microservices ecosystem demands higher-order distributed computing capabilities, such as remote service invocation, distributed schedulers and executors, live object mapping, or map-reduce frameworks.
  • You have connection pool fatigue: manually managing thread safety and connection pooling has led you to constantly borrow and return individual Jedis instances to the pool.

What Redisson Adds on Top of Jedis

If you find yourself in any of the scenarios listed above, you need a Java client that offers more than just the ability to issue raw Valkey/Redis commands. Here are some of the things Redisson can do on top of that:

Enterprise-Grade Distributed Locks and Synchronizers

Implementing a distributed lock in Jedis means you must acquire the lock using jedis.set(key, token, SetParams.setParams().nx().px(30000)) and subsequently execute a Lua script to ensure the key is only deleted if the token matches your session. Furthermore, you are responsible for programming ownership validation, reentrancy logic, and lease renewal mechanisms. You are also responsible for testing failover conditions. Getting this single-instance pattern correct in production is notoriously difficult.

Redisson abstracts this into just three lines with its RLock object, just one of many locks and synchronizers:

RLock lock = redisson.getLock("order:1234");
lock.lock();
try {
    // critical section, safe across every node and JVM
} finally {
    lock.unlock();
}

Behind the scenes, Redisson implements a sophisticated watchdog mechanism that automatically extends the lock lease as long as the holding thread remains alive. This watchdog defaults to a 30-second interval (configurable via Config.lockWatchdogTimeout) and activates when lock() is invoked without an explicitly defined lease. This ensures that prolonged background tasks do not prematurely lose their locks, while simultaneously guaranteeing that a crashed instance will not hold the lock indefinitely.

Managed Near-Cache and JCache Implementations

Jedis introduced client-side caching in version 5.2.0, but this implementation caches command outputs on the client side and manages invalidation via RESP3 push messages, leveraging the CLIENT TRACKING feature. However, the developer is still tasked with configuring a Cache and defining the specific Cacheable policy to determine exactly which commands and keys should be stored. In addition, this mechanism strictly requires the RESP3 protocol and Redis version 7.4 or later.

Meanwhile, Redisson delivers a fully managed near-cache solution through the RLocalCachedMap object, which is deeply integrated into its broader map object model. This managed layer handles complex invalidation and eviction policies (including LRU, LFU, soft, and weak references), Time-To-Live (TTL) parameters, and automatic cross-instance synchronization.

Seamless Integration of Java Collections

Whereas Jedis only allows developers to work with Valkey/Redis commands, placing the responsibility of parsing and mapping the raw results on them, Redisson offers strictly typed Java objects that natively implement familiar interfaces. For example, RMap seamlessly implements java.util.concurrent.ConcurrentMap, RList fulfills the contract for List, RSet acts as a standard Set. Classes like RQueue, RDeque, and RBlockingQueue implement their respective Java queue interfaces.

With over 50 unique objects available in Redisson, your application's domain logic can abandon command-centric thinking and operate purely within the paradigm of standard Java collections.

An Advanced Distributed Services Platform

Redisson goes far beyond what Jedis can offer by providing its own suite of distributed microservices. This includes a robust remote-invocation (RPC) framework, a Live Object service designed to seamlessly map nested Java objects into Redis or Valkey hash structures, a fully distributed scheduler and executor service, and a comprehensive MapReduce engine.

With Redisson PRO, this list of services expands to include enterprise-grade, reliable messaging features, such as Reliable Pub/Sub and Reliable Queues, which form the foundation of a fully TCK-passing JMS provider.

Redisson is Compatible With Both Valkey and Redis

One of the most common questions teams have before considering a migration to Redisson is whether it will work with their particular infrastructure, which might have Valkey or Redis.

Redisson is both a Valkey and a Redis client, compatible with Redis 3.0 or higher, and Valkey 7.2.5 or newer. The client works equally well with redis://, rediss://, valkey://, and valkeys:// URLs. In addition, Redisson provides extensive support for fully managed cloud environments, seamlessly integrating with AWS ElastiCache, AWS MemoryDB, Azure Cache, and Google Cloud Memorystore.

Migrating From Jedis to Redisson: A Step-by-Step Guide

Migrating from Jedis to Redisson is not difficult, but it does require a significant number of changes. Just follow these steps, paying close attention to the specifics of your infrastructure:

Step 1: Swap the Dependency

Update your build config to remove Jedis and add Redisson:

<!-- remove -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

<!-- add -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>4.6.0</version>
</dependency>

If you use Gradle in your environment, the implementation string is implementation 'org.redisson:redisson:4.6.0'.

In a Spring Boot ecosystem, it's important to note that the default spring-boot-starter-data-redis component utilizes the Lettuce client. If you are currently using Jedis, you previously made an explicit choice to include it and likely manually configured a JedisConnectionFactory. To transition effectively, remove both the Jedis library and your custom factory, and replace them with Redisson's dedicated Spring Boot starter.

This starter automatically registers a fully compliant RedissonConnectionFactory, ensuring that any existing Spring RedisTemplate implementations continue to function without requiring rewrites:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>4.5.0</version>
</dependency>

Step 2: Replace Connection Lifecycles and Client Construction

This step features the most significant structural change. A standard Jedis instance is fundamentally designed to wrap a single network connection and is inherently not thread-safe.

Redisson provisions a single, inherently thread-safe RedissonClient interface. While newer versions of Jedis introduced UnifiedJedis and JedisPooled classes that manage pooling, they still lack an overarching object model as Redisson does.

The Redisson client is instantiated once, shared globally across your application as a singleton, and then it abstracts away the mechanics of connection pooling:

// Jedis — borrow a connection per unit of work; Jedis is not thread-safe
JedisPool pool = new JedisPool(new HostAndPort("127.0.0.1", 6379),
        DefaultJedisClientConfig.builder().build());
try (Jedis jedis = pool.getResource()) {
    jedis.set("user:1", "Ada");
}

// Redisson — one shared, thread-safe client; no borrow/return
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // or valkey://
RedissonClient redisson = Redisson.create(config);

For topology mappings, Jedis's JedisCluster maps to Redisson's config.useClusterServers().addNodeAddress(...). A JedisSentinelPool translates to config.useSentinelServers().setMasterName(...).addSentinelAddress(...). For standard primary/replica architectures, you can use either config.useMasterSlaveServers() or config.useReplicatedServers(). For lifecycle management, closing the JedisPool is replaced by invoking redisson.shutdown().

Database selection and authentication are managed directly in the server configuration object. Use setUsername(...) and setPassword(...) to pass ACL credentials, and utilize setDatabase(int) to target a specific logical database. This approach is similar to that of Jedis's DefaultJedisClientConfig, keeping in mind that Redis Cluster environments do not support database indexing.

Step 3: Translate Commands Into Objects

The bulk of the migration workload happens in this step. Whereas Jedis executes methods representing commands against a connection (jedis.get("k")), Redisson requires you to acquire a modeled object and invoke methods upon it.

For strings and basic values:

// Jedis
jedis.set("user:1", "Ada");
String name = jedis.get("user:1");

// Redisson
RBucket bucket = redisson.getBucket("user:1");
bucket.set("Ada");
String name = bucket.get();

Set operations with conditional writes (SETEX/SETNX):

// Jedis — SetParams covers TTL and conditional writes
jedis.setex("k", 60, "v");
jedis.set("k", "v", SetParams.setParams().nx());

// Redisson — direct methods on RBucket
bucket.set("v", Duration.ofSeconds(60));   // SETEX
boolean created = bucket.trySet("v");      // SETNX

For binary values and partial updates (APPEND / GETRANGE / SETRANGE):

// Jedis
jedis.append("log", "entry");
String slice = jedis.getrange("log", 0, 3);
jedis.setrange("log", 0, "PREF");

// Redisson — RBinaryStream exposes the value as a stream and a channel
RBinaryStream stream = redisson.getBinaryStream("log");
ByteBuffer buf = ByteBuffer.allocate(4);
stream.getOutputStream().write("entry".getBytes());     // APPEND
stream.getChannel().write(buf);                         // SETRANGE
stream.getChannel().read(buf);                          // GETRANGE

When performing range operations in Redisson, you must manually seek the channel to your desired target offset before executing the read/write. The getAsynchronousChannel() method is available for non-blocking equivalents.

Atomic counters:

// Jedis
jedis.incr("visits");
jedis.incrBy("visits", 5);

// Redisson
RAtomicLong visits = redisson.getAtomicLong("visits");
visits.incrementAndGet();
visits.addAndGet(5);

Key expiration and lifecycle management:

// Jedis
jedis.expire("user:1", 60);
jedis.del("user:1");
boolean exists = jedis.exists("user:1");
jedis.rename("user:1", "user:2");

// Redisson — per-object methods for a single key…
RBucket b = redisson.getBucket("user:1");
b.expire(Duration.ofSeconds(60));
b.delete();
boolean present = b.isExists();
b.rename("user:2");

// …and RKeys for bulk or pattern operations
RKeys keys = redisson.getKeys();
keys.delete("user:1", "user:2");
Iterable matched = keys.getKeysByPattern("user:*");

Hashes to Java maps:

// Jedis
jedis.hset("user:1", "name", "Ada");
String n = jedis.hget("user:1", "name");

// Redisson — RMap implements java.util.concurrent.ConcurrentMap
RMap user = redisson.getMap("user:1");
user.put("name", "Ada");
String n = user.get("name");

Sets, Lists, and Sorted Sets:

// Jedis
jedis.rpush("tasks", "first");
jedis.sadd("tags", "java");
jedis.zadd("leaderboard", 100.0, "alice");

// Redisson — each implements the matching java.util type
RList list = redisson.getList("tasks");
list.add("first");
RSet set = redisson.getSet("tags");
set.add("java");
RScoredSortedSet board = redisson.getScoredSortedSet("leaderboard");
board.add(100.0, "alice");

Pipelining and transactions:

// Jedis — MULTI/EXEC and pipelining
Transaction tx = jedis.multi();
tx.set("a", "1");
tx.set("b", "2");
tx.exec();

Pipeline p = jedis.pipelined();
p.set("a", "1");
p.set("b", "2");
p.sync();

// Redisson — RBatch pipelines; RTransaction adds ACID semantics
RBatch batch = redisson.createBatch();
batch.getBucket("a").setAsync("1");
batch.getBucket("b").setAsync("2");
batch.execute();

Raw scripting executions:

// Jedis
jedis.eval("return 1", 0);

// Redisson
RScript script = redisson.getScript();
Long r = script.eval(RScript.Mode.READ_ONLY, "return 1", RScript.ReturnType.LONG);

Pub/sub communication models:

// Jedis — JedisPubSub; subscribe() BLOCKS the calling thread until unsubscribed
JedisPubSub listener = new JedisPubSub() {
    @Override public void onMessage(String channel, String message) { handle(message); }
};
jedis.subscribe(listener, "news");   // run this on a dedicated thread
// publish from another connection: jedis.publish("news", "hello");

// Redisson — non-blocking listener registration on the client you already have
RTopic topic = redisson.getTopic("news");
topic.addListener(String.class, (channel, msg) -> handle(msg));
topic.publish("hello");

Step 4: Transition to Asynchronous and Reactive Paradigms

A common misconception about migrating away from Jedis is that you must shift to asynchronous programming. If you picked Jedis for the simplicity of blocking I/O, you can maintain that architectural stance with Redisson. However, if your apps now require non-blocking throughput, Redisson exposes reactive interfaces directly on the same objects.

Invoking redisson.reactive().getBucket("k") yields native Reactor-based reactive objects, while redisson.rxJava() delivers an RxJava3 compliant API. Additionally, virtually every Redisson object includes native ...Async methods that return an RFuture. The foundational object model remains consistent. You just adjust your calling style to meet your concurrency requirements.

For the exhaustive list, 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.

Technical Considerations and Implementation Pitfalls

A migration on this scale always has some potential pitfalls. Here are some of the main technical considerations and how to avoid post-migration problems:

Threading and Connection Models

The biggest change when moving from Jedis to Redisson is not in the threading model but in the connection model. Instead of borrowing and returning individual Jedis instances, you instantiate and globally share a single RedissonClient singleton, which manages the underlying connection pool. Because both clients utilize a default blocking behavior, your existing call sites will largely retain their synchronous architectural shape. The transition doesn't mean an automatic switch to asynchronous execution unless you explicitly make this change.

Non-Blocking Pub/Sub Mechanics

In Jedis, invoking jedis.subscribe(JedisPubSub, channels) actively blocks the executing thread for the entire duration of the subscription. This necessitates using dedicated threads for listening and separate connections for unsubscribing. Redisson addresses this bottleneck as RTopic.addListener(...) registers the listener in the background, freeing up the calling thread for further operations.

Cluster Placement and Partitioning

In a clustered topology, a single Redisson object (such as a map or set) resides on the master node by default. Attempting to distribute or partition a single, massive data structure across multiple disparate cluster nodes is officially classified as a data partitioning capability. Note, however, that this functionality is only available in Redisson PRO.

Similar articles