Redis with Spring Boot: A Guide to Spring Data Redis
You did not choose Spring Data Redis. Someone said the database was too slow, you added spring-boot-starter-data-redis, autowired a RedisTemplate, and it worked on the first run. That is the whole decision most teams make about it.
Which is fine, until three things surface at once. It is an abstraction over a Redis client rather than a client itself, and you have never chosen which one sits underneath. Its default serializer writes Java binary into your keyspace. And version 4.0 changed defaults on both layers in ways that compile cleanly and fail later.
This guide is about the parts underneath the parts you use, and everything here applies equally to Valkey. Redisson, which we build, is one of the three connection factories Spring Data Redis can run on; this guide is about running Spring Data Redis well, and the comparison waits until the end.
What Spring Data Redis Actually Is
Spring Data Redis is not a protocol client. It is three layers, and most of the confusion around it comes from treating the top layer as though it were the bottom one.
| Layer | What it is | What you configure |
|---|---|---|
| Driver | Lettuce or Jedis. The thing that actually speaks the Redis protocol. | Pooling, timeouts, TLS, topology refresh |
RedisConnectionFactory | The SPI between the two. One implementation per driver. | Which driver, and how it is built |
| Templates and repositories | RedisTemplate, CrudRepository, @Cacheable. | Serializers, keyspaces, TTL |
Spring Boot picks the driver for you, builds the factory for you, and most applications never touch either. That is the pattern this guide keeps returning to: a default chosen three layers down changes behaviour you observe at the top, and the documentation for the top layer does not mention it.
The middle layer is an interface, and three implementations of it are in common use. Two wrap drivers: Lettuce's and Jedis's. The third is Redisson's, a full client with its own object layer that also satisfies the same SPI.
Spring Cache is a different abstraction again: @Cacheable and CacheManager belong to Spring Framework, and Redis is one provider among several. Our glossary entry on Spring Cache covers that split, and our guide to Spring Boot Redis caching covers the caching half. This guide does not re-explain @Cacheable.
Which Version You Are On
Open your build file first, because the answer decides which half of this guide applies to you.
| Spring Boot | Spring Data Redis | JSON library | Note |
|---|---|---|---|
| 3.2.x | 3.2.x | Jackson 2 | What most tutorials on the first page of Google are built on. |
| 3.5.x | 3.5.x (3.5.13 current) | Jackson 2 | The last 3.x line. Free support ended 30 June 2026. |
| 4.0.x | 4.0.x | Jackson 3 | Shipped 20 November 2025. The breaking changes in the next-but-one section land here. |
| 4.1.1 | 4.1.1 | Jackson 3 | Current. Adds annotation-driven pub/sub listeners. |
You do not pin Spring Data Redis directly. Spring Boot's dependency management pins spring-data-bom, which pins the Redis module: Boot 4.1.1 manages Spring Data BOM 2026.0.1, resolving spring-data-redis to 4.1.1. To know what you are on without guessing, ask Maven:
mvn dependency:tree -Dincludes=org.springframework.data:spring-data-redis
One related version note if Redisson is anywhere in your build: its redisson-spring-data-NN modules are numbered after the Spring Data Redis line rather than the Spring Boot one, so -41 pairs with Spring Data Redis 4.1.x.
Everything below is written against Spring Data Redis 4.1.1 on Spring Boot 4.1.1, with 3.x differences called out where they matter.
RedisTemplate, and the Serializer Decision
The starter gives you two template beans without being asked: a RedisTemplate<Object, Object> and a StringRedisTemplate. Both wrap the same connection factory, and they differ in one thing, which happens to be the thing that matters most.
You reach data through an operation view, one per Redis type:
@Autowired StringRedisTemplate redis;
redis.opsForValue().set("book:9780134685991", "Effective Java");
String title = redis.opsForValue().get("book:9780134685991");
redis.opsForHash().put("user:1", "name", "Ada");
redis.opsForList().rightPush("queue:jobs", "job-17");
redis.opsForSet().add("tags:java", "concurrency", "jvm");
redis.opsForZSet().add("leaderboard", "ada", 4200);
The full set is opsForValue, opsForList, opsForSet, opsForZSet, opsForHash, opsForGeo, opsForHyperLogLog, opsForStream and opsForCluster. Each has a bound* sibling, such as boundValueOps("key"), that takes the key once rather than on every call.
Set a value with an expiry in the same call:
redis.opsForValue().set("session:abc", payload, Duration.ofMinutes(30));
boolean claimed = Boolean.TRUE.equals(
redis.opsForValue().setIfAbsent("lock:order:17", "owner", Duration.ofSeconds(10)));
A currency note for anyone copying from older material: the (long, TimeUnit) overloads of set, setIfAbsent and setIfPresent were deprecated in Spring Data Redis 4.1. Use Duration, or Expiration when you need KEEPTTL semantics.
That setIfAbsent call deserves a caveat, because it is the most-copied snippet in this ecosystem: it is a lock only in the loosest sense, with no owner check on release, no lease renewal and no fencing token. Distributed locking is harder than it looks, and Spring Data Redis does not model it.
The default serializer is Java serialization
Here is the difference between the two templates. StringRedisTemplate serializes keys and values with StringRedisSerializer in UTF-8. Plain RedisTemplate defaults to JdkSerializationRedisSerializer, which is Java's native binary format.
Three consequences follow, and they all arrive late:
- Every stored class must implement
Serializable, including everything it transitively references. - Values are opaque binary.
GETfromredis-clitells you nothing, which makes debugging a correctness problem far harder than it should be. - The format is tied to the class. Add a field, redeploy, and previously stored entries may fail to deserialize: a cold cache at best, an exception storm at worst.
So most teams move to JSON. On Spring Data Redis 4.x that means the Jackson 3 serializer, which has no no-argument constructor. You build it:
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
GenericJacksonJsonRedisSerializer json = GenericJacksonJsonRedisSerializer.builder()
.enableDefaultTyping(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build())
.build();
template.setValueSerializer(json);
template.setHashValueSerializer(json);
template.afterPropertiesSet();
return template;
}
String keys with JSON values is the combination you almost always want: the keyspace stays readable in redis-cli, and the values survive a field being added. Set the four serializers independently, or set setDefaultSerializer once and override the ones that differ. If you build the template from the no-argument constructor and setters, call afterPropertiesSet(); the constructor that takes a connection factory calls it for you.
There is a shortcut worth understanding before you reach for it. RedisSerializer.json() returns a GenericJacksonJsonRedisSerializer built with enableUnsafeDefaultTyping(), which accepts type information for any class on the wire and gives you no way to narrow it. Building the serializer yourself costs four lines and leaves the validator in your hands.
One structural difference worth knowing if you ever compare the options: Spring Data Redis sets serializers per template, so one RedisTemplate has a single value format for everything it touches. Redisson sets its codec per object, so a map and a queue in the same application can use different formats.
What you are choosing here is a wire format, and that choice outlives the code. Our guide to serialization codecs covers the trade-offs, and the glossary entry on serialization covers the ground rules.
Spring Data Redis 4.0: What Changed Underneath You
Spring's own migration guide documents this well. Nobody else has written it up, and every third-party guide currently ranking for Spring Data Redis is on 3.x.
Jackson 2 became Jackson 3
Spring Data Redis 4.0 uses Jackson 3 as its primary JSON library, and Jackson 3 moved its package root from com.fasterxml.jackson to tools.jackson. The annotations stay where they were. The serializer classes were renamed to match:
| Purpose | Spring Data Redis 3.x | Spring Data Redis 4.x |
|---|---|---|
| Generic JSON serializer | GenericJackson2JsonRedisSerializer | GenericJacksonJsonRedisSerializer |
| Typed JSON serializer | Jackson2JsonRedisSerializer | JacksonJsonRedisSerializer |
| Hash mapper | Jackson2HashMapper | JacksonHashMapper |
| Reader / writer | Jackson2ObjectReader / Writer | JacksonObjectReader / Writer |
The Jackson 2 classes are still present and deprecated, which is deliberate: you need them to read data the old serializer wrote.
The rename is not a drop-in, and this is the trap
GenericJackson2JsonRedisSerializer enabled default typing automatically, with no PolymorphicTypeValidator required. GenericJacksonJsonRedisSerializer does not.
So a find-and-replace across your configuration compiles, starts, and writes JSON with no embedded type header. Values typed as Object or as a polymorphic base come back as LinkedHashMap instead of your class. Nothing throws at startup; the failure is a ClassCastException downstream, or silently wrong data.
Two things to do, in order. Enable default typing deliberately with a validator, as in the previous section. Then keep reading existing entries with the old serializer until they have expired or been rewritten: Jackson 3 can emit JSON that differs from Jackson 2's output, and a serializer swap does not migrate what is already in the keyspace.
The rest of the 4.0 surface
| What changed | Detail |
|---|---|
RedisSerializer.serialize | Must now return a non-null byte array. Custom implementations return an empty array for null. deserialize may still return null. |
StringRedisTemplate callbacks | No longer exposes the connection as StringRedisConnection. Use the template's own operations instead. |
| Cluster and Sentinel config | new RedisClusterConfiguration(PropertySource) becomes RedisClusterConfiguration.of(PropertySource); same for RedisSentinelConfiguration. The ordinary constructors are unaffected. |
| Lettuce observability | The built-in adapter was removed in favour of Lettuce's own Micrometer integration, configured on ClientResources. |
| Assorted renames | BoundSetOperations.diff becomes difference; hasExplictTimeToLiveProperty gains its missing i. |
| Nullability | JSpecify annotations throughout, so a strict IDE or build will flag things it previously ignored. |
Two cache-side changes belong to this release too: an argument-order swap on a RedisCacheManager constructor, and a change to how cache writes are dispatched. The second is covered below under the driver, because that is where its trigger lives.
Repositories: @RedisHash, Indexes and TTL
Alongside the template, Spring Data Redis offers the repository model you know from JPA. It is the least-discussed half of the project and the one with the sharpest edges.
@RedisHash("people")
public class Person {
@Id private String id;
@Indexed private String firstname;
private String lastname;
private Address address;
@TimeToLive private Long expiration;
}
public interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByFirstname(String firstname);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findTop5ByFirstname(String firstname);
}
Note which @Id this is: org.springframework.data.annotation.Id, from Spring Data Commons, not the JPA one. @RedisHash and @TimeToLive come from org.springframework.data.redis.core, and @Indexed from org.springframework.data.redis.core.index. In a Spring Boot application you do not need @EnableRedisRepositories. The starter auto-configures repositories when a connection factory is present, and spring.data.redis.repositories.enabled=false turns that off.
What a save actually writes
The cost model is not obvious from the annotations. Saving one Person with one indexed field writes four things:
HMSET "people:19315449-cda2-4f5c-b696-9cb8018fa1f9" "_class" "Person" "id" "…" "firstname" "rand" "lastname" "al'thor"
SADD "people" "19315449-…"
SADD "people:firstname:rand" "19315449-…"
SADD "people:19315449-…:idx" "people:firstname:rand"
The entity is a hash at keyspace:id. Then a set of every id in the keyspace, so findAll has something to read. Then one set per indexed value, which is what makes findByFirstname possible. Then a helper set naming the indexes this entity appears in, so that updating or deleting it can clean up entries that no longer apply.
Queries resolve through those sets: findByFirstname issues SINTER against the index set, then HGETALL for each id. And intersects, Or unions. That is the whole query engine.
The four limits worth knowing before you commit
- The query vocabulary is tiny.
And,Or,Is/Equals,IsTrue,IsFalse, andTop/First. No ranges, noBetween, noLike, no joins. Anything outside that list is not a derived query. - Sorting happens in your JVM. Redis does not sort hashes on retrieval, so the repository fetches the matches and applies a
Comparatorin process.findByFirstnameOrderByAgeDescworks; it just does not work in Redis. - A save is destructive and then rewritten. Updating an existing entity issues
DELfollowed byHMSET, so any field not in the mapping is gone. When you want to touch one field, build aPartialUpdateand pass it to the key-value template'supdate(...)instead. - References are shallow. A property marked
@Referencestores a key pointer rather than a copy, and the referenced object is not persisted when the referencing one is saved. You save it yourself. Indexes cannot be resolved on references.
Expiry, and the phantom copy
@TimeToLive sets a per-entity expiry, and being a Redis TTL, the entity vanishes when it fires. The index sets do not. They are separate keys, and a member pointing at a key that no longer exists is how a stale index happens.
Spring Data Redis handles that with keyspace notifications, and the mechanism is unusual enough to state plainly. When expiry is in use it persists a phantom copy of the entity, set to expire five minutes after the original. That copy is what lets the expiry event carry the old value, so the cleanup knows which index entries to remove.
The listener is off by default, since enableKeyspaceEvents on @EnableRedisRepositories defaults to OFF. Turn it on if you rely on RedisKeyExpiredEvent, or on indexes staying clean as entities expire.
If the object mapping is what you want and the query vocabulary is not, Redisson's Live Object service maps annotated objects onto Redis structures without a derived-query layer on top. Our Redis object mapper on Java takes that approach.
The Driver Underneath: Lettuce or Jedis
Spring Data Redis does not talk to Redis. One of these does it for you:
LettuceConnectionFactory | JedisConnectionFactory | |
|---|---|---|
| Spring Boot default | Yes | No |
| Connection model | One thread-safe multiplexed connection | Connection per thread, pooled |
| Pool needed? | Usually not | Always |
Implements ReactiveRedisConnectionFactory | Yes | No |
| Default timeouts | 60s connect on the pooling configuration | 2000 ms connect, 2000 ms read |
| Master/replica reads in Boot auto-config | Yes | No |
Switching is a property plus a dependency change: set spring.data.redis.client-type to jedis, put Jedis on the classpath and take Lettuce off it. The property has existed since Spring Boot 4.0 and accepts exactly two values.
spring:
data:
redis:
client-type: jedis # or lettuce, the default
For anything beyond host and port, build the factory yourself. It is the only way to reach driver settings that have no Spring Boot property:
@Bean
LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(2))
.shutdownTimeout(Duration.ofMillis(200))
.useSsl().and()
.build();
return new LettuceConnectionFactory(
new RedisStandaloneConfiguration("redis.internal", 6379), clientConfig);
}
Two driver settings decide how this behaves when Redis is unwell, and neither has a Spring Data Redis property. Lettuce's command timeout is generous by default, and a request thread blocked on Redis serves nothing else. Its request queue is unbounded unless you bound it, so an outage becomes a heap problem rather than a fast failure. Both live in our Lettuce guide; the Jedis equivalents are in the Jedis guide, and Jedis versus Lettuce covers the choice itself.
useSsl() above turns TLS on; the certificate and hostname rules belong to the driver, and our guide to Redis over TLS on Java covers what managed services need.
Your driver choice changes your cache consistency
This is the sharpest consequence of the layering, and the reason the driver belongs in a guide most people open for RedisTemplate. In Spring Data Redis 4.0 the default RedisCacheWriter performs put, evict and clear asynchronously when the connection factory implements ReactiveRedisConnectionFactory. From the migration guide:
"If the configuredRedisConnectionFactoryimplementsReactiveRedisConnectionFactory, the defaultRedisCacheWriteruses asynchronous behavior forput(…),evict(…), andclear(…). These operations may complete after the calling method returns."
LettuceConnectionFactory implements it. JedisConnectionFactory does not. Lettuce is the default.
So a stock spring-boot-starter-data-redis application with @EnableCaching and no reactive code anywhere got asynchronous cache writes when it moved to Boot 4, and the reference documentation for RedisCache does not mention it. Two applications on identical code get different consistency depending on which driver is on the classpath. Restore the old behaviour explicitly:
RedisCacheWriter cacheWriter = RedisCacheWriter.create(connectionFactory,
configurer -> configurer.immediateWrites());
Cache.evictIfPresent and Cache.invalidate stay synchronous either way. The cache-side consequences are in our Spring Boot Redis cache guide; what matters here is that the trigger sits three layers below the annotation.
Everything in this section belongs to the driver, which also means none of it reaches the third implementation named earlier. Redisson does its own connection handling and pooling, so spring.data.redis.lettuce.pool.* and its Jedis counterpart have nothing to configure there.
spring.data.redis.*: What the Properties Reach
If your spring.redis.* properties stopped working, this is why: the prefix became spring.data.redis.* in Spring Boot 3.0, because Redis auto-configuration requires Spring Data on the classpath. Nothing about that changed in Boot 4.
spring:
data:
redis:
host: redis.internal
port: 6379
username: app
password: secret
database: 0
timeout: 2s
connect-timeout: 1s
client-name: orders-service
client-type: lettuce
ssl:
enabled: true
lettuce:
pool:
enabled: true
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 500ms
Three behaviours in there that are not obvious from the key names:
urlwins. Setspring.data.redis.urlandhost,port,usernameandpasswordare all ignored. Setting both is a common way to spend an afternoon.- Pooling is classpath-driven. A pooled connection factory is auto-configured when
commons-pool2is present. On Lettuce that is usually not what you want, because a multiplexed connection does not need one. - Cluster and Sentinel live under
spring.data.redis.cluster.*andspring.data.redis.sentinel.*, and reaching them through properties is the easy path; reaching them through a hand-built factory is the flexible one.
The Boot 4 rename that catches people
Here is a distinction worth holding onto, because it is asymmetric. Spring Boot 4 modularised auto-configuration, and the Redis classes moved and changed name:
| Spring Boot 3.5 | Spring Boot 4.x | |
|---|---|---|
| Properties class | RedisProperties | DataRedisProperties |
| Auto-configuration | RedisAutoConfiguration | DataRedisAutoConfiguration |
| Package | o.s.boot.autoconfigure.data.redis | o.s.boot.data.redis.autoconfigure |
| Property prefix | spring.data.redis | spring.data.redis — unchanged |
The YAML did not move; the Java did. A configuration review finds nothing and the build fails on an import, which is the better failure of the two. It still surprises people who were told Boot 4 changed the Redis properties. It did not. It changed the classes that read them.
One related prefix that has never moved, which catches people who assume everything did: Redisson's own settings live under spring.redis.redisson.*, unchanged through both Boot 3.0 and Boot 4.
Pub/Sub, and What Boot 4.1 Now Does For You
The long-standing way to receive messages is a container plus an adapter:
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory factory,
MessageListenerAdapter adapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(adapter, ChannelTopic.of("orders"));
container.addMessageListener(adapter, PatternTopic.of("orders.*"));
return container;
}
@Bean
MessageListenerAdapter adapter(OrderListener listener) {
return new MessageListenerAdapter(listener, "handleMessage");
}
Note the static factories ChannelTopic.of(...) and PatternTopic.of(...), which is what the current documentation uses, and that MessageListenerAdapter lives in org.springframework.data.redis.listener.adapter, one package deeper than the container.
Spring Boot 4.1 and Spring Data Redis 4.1 made most of that unnecessary. There is now an annotation-driven model, and Boot registers a default container when your application does not define one:
@Component
public class OrderListener {
@RedisListener(topic = "orders")
public void onOrder(String payload) {
// ...
}
}
The annotation is repeatable, supports patterns, and accepts the spring-messaging parameter conventions including @Payload, @Header and validation. The starter now pulls in spring-messaging for that reason. Container behaviour is tunable through spring.data.redis.listener.*, and RedisMessageListenerContainerConfigurer builds additional containers consistently with the auto-configured one.
One property of Redis pub/sub that no amount of Spring convenience changes: it is fire-and-forget. A subscriber that is down when a message is published never sees it, with no acknowledgement and no replay. When you need delivery guarantees, reach for streams or a queue. Our walkthrough of Redis pub/sub in Java covers the semantics in full.
That is a property of the server rather than of Spring, so it survives changing clients: Redisson's RTopic is fire-and-forget for the same reason. Its Reliable Pub/Sub, a PRO feature, is a different structure with delivery guarantees rather than a wrapper over PUBLISH.
Transactions, and the enableTransactionSupport Trap
Redis transactions queue commands between MULTI and EXEC and run them as one unit. They are not rollback-capable in the database sense, and they need one connection throughout, which is the problem: RedisTemplate does not promise you one.
"RedisTemplate is not guaranteed to run all the operations in the transaction with the same connection."
Hence SessionCallback, which binds one connection for the duration:
List<Object> results = redis.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForSet().add("tags:java", "concurrency");
operations.opsForValue().increment("tags:java:count");
return operations.exec();
}
});
The trap is the other mechanism. setEnableTransactionSupport(true) makes a template participate in Spring-managed transactions, binding the connection to the current transaction in a ThreadLocal and issuing EXEC or DISCARD at the boundary. It is off by default and set per template, so turning it on for the auto-configured StringRedisTemplate means declaring your own bean.
Then the part people discover late: Spring Data Redis ships no PlatformTransactionManager. Transaction support joins somebody else's transaction, typically a DataSourceTransactionManager from the JDBC side. With @Transactional on a method and no transaction manager in the application, there is nothing for Redis to join and the flag does nothing you can observe. Our guide to transactions in Redis on Java covers what the server guarantees.
This is one of the few places where the third factory changes what is possible rather than how it is written. Redisson ships RedissonTransactionManager, which plugs into @EnableTransactionManagement, so @Transactional has something to drive without a JDBC datasource in the application; ReactiveRedissonTransactionManager does the same for WebFlux.
Reactive: ReactiveRedisTemplate
The reactive starter gives you ReactiveStringRedisTemplate, and every operation view returns Reactor types:
@Autowired ReactiveStringRedisTemplate redis;
Mono<Boolean> stored = redis.opsForValue()
.set("book:9780134685991", "Effective Java", Duration.ofHours(1));
Flux<String> tags = redis.opsForSet().members("tags:java");
One constructor detail catches people building the generic template by hand: ReactiveRedisTemplate has no connection-factory-only constructor. A RedisSerializationContext is mandatory, since there is no sensible default for arbitrary key and value types. ReactiveStringRedisTemplate supplies the string context for you.
The constraint that decides whether any of this is available: only Lettuce supports the reactive API. JedisConnectionFactory does not implement ReactiveRedisConnectionFactory, so choosing Jedis rules reactive out entirely. Our guide to non-blocking Redis on Java covers the wider picture across Reactor, RxJava and WebFlux.
That constraint belongs to the abstraction rather than to Redis. Redisson exposes async, reactive and RxJava variants of every object it offers, independently of Spring Data Redis's reactive support.
Cluster and Sentinel
Both topologies are reachable through properties, which is the right starting point:
spring:
data:
redis:
cluster:
nodes: redis-1:6379,redis-2:6379,redis-3:6379
max-redirects: 3
sentinel:
master: mymaster
nodes: sentinel-1:26379,sentinel-2:26379,sentinel-3:26379
In code you build a configuration object and hand it to the factory. Spring Data Redis 4.0 replaced the PropertySource constructors with static factories, the change most likely to break an existing configuration class:
// 3.x
RedisClusterConfiguration config = new RedisClusterConfiguration(propertySource);
// 4.x
RedisClusterConfiguration config = RedisClusterConfiguration.of(propertySource);
// Unchanged in both
RedisClusterConfiguration explicit =
new RedisClusterConfiguration(List.of("redis-1:6379", "redis-2:6379"));
Only the PropertySource constructors moved; the ordinary ones are unaffected. RedisSentinelConfiguration changed identically.
What the abstraction does not expose is the operational half. Topology refresh on Redis Cluster is a driver setting, off by default on Lettuce, so a cluster that reshards or fails over can leave a long-lived client sending commands to a node that no longer owns the slot. There is no Spring Data Redis property for it. Our guides to connecting to a Redis cluster in Java and to Redis Sentinel cover both topologies.
When You Need More Than the Abstraction
Everything above is worth doing, and for a large share of applications it is the whole job. Spring Data Redis models commands and it models caching; if what you need is opsForValue().get() and @Cacheable, you are finished.
What it does not model is coordination. There is no lock API, no distributed collection, no near cache. The moment you need one you are writing it yourself on SET NX PX, and as the earlier setIfAbsent example showed, the naive version has no owner check, no lease renewal and no fencing.
Redisson is the third RedisConnectionFactory implementation, which makes it unusual here: adopting it does not mean rewriting anything. RedissonConnectionFactory implements the same interface Lettuce's and Jedis's do, so every RedisTemplate call site and every @Cacheable annotation keeps working, and RedissonClient becomes available alongside them.
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>4.7.0</version>
</dependency>
Because both implement RedisConnectionFactory, you run one or the other. Exclude Lettuce so there is 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>
// Existing code, untouched
@Autowired RedisTemplate<String, String> redisTemplate;
redisTemplate.opsForValue().set("user:1", "Ada");
// Available alongside it
@Autowired RedissonClient redisson;
RLock lock = redisson.getLock("order:1234");
lock.lock();
try {
// critical section, held across every node and JVM
} finally {
lock.unlock();
}
The lock renews its lease while the holder is alive and releases it if the holder dies, which is what the hand-rolled version gets wrong. Beyond that:
| Need | Spring Data Redis | Redisson | Licence |
|---|---|---|---|
| Distributed lock | Not modelled | RLock, plus fair, read/write, fenced and multi-locks, semaphores and latches | Open-source |
| Java collections on Redis | opsForX() views | RMap implements ConcurrentMap, RList, RSet, RQueue — 60+ objects | Open-source |
| Per-entry TTL and max-idle | Entity-level @TimeToLive only | RMapCache, RMapCacheNative | Open-source |
| Near cache | — | RLocalCachedMap | Open-source |
| Spring Cache manager | RedisCacheManager | RedissonSpringCacheManager, with per-cache TTL and max-idle | Open-source |
| JCache provider | — | Full JSR-107 implementation | Open-source |
Near cache behind @Cacheable | — | RedissonSpringLocalCachedCacheManager | PRO |
| Clustered cache managers | — | RedissonClusteredSpringCacheManager, RMapCacheV2 | PRO |
On Spring Boot 4 the starter bundles redisson-spring-data-41, which targets Spring Data Redis 4.1; Redisson is not managed by Spring Boot's dependency BOM, so the version is yours to pin, and our guide to Redisson on Spring Boot 4 has the full matrix. The Spring Data Redis to Redisson guide walks through both the drop-in path and the native-object path, and the feature comparison sets the two side by side.
Frequently Asked Questions
Is Redis Used in Spring Boot?
Yes, and it is the most common cache and session store in the Spring ecosystem. You add spring-boot-starter-data-redis, point spring.data.redis.host at a server, and Spring Boot auto-configures a connection factory, a RedisTemplate and a StringRedisTemplate. From there you can use Redis through the template, through repositories, as the backing store for @Cacheable, or as an HTTP session store through Spring Session.
What Is Spring Data Redis Used For?
It is the integration layer between a Spring application and Redis or Valkey. It provides RedisTemplate for running commands, a repository model for mapping objects to Redis hashes, a CacheManager for Spring's caching abstraction, and a listener container for pub/sub. It is not a Redis client itself; it sits on top of Lettuce or Jedis, which do the network work.
What Is the Difference Between RedisTemplate and StringRedisTemplate?
The serializers. StringRedisTemplate is a RedisTemplate<String, String> that serializes keys and values as UTF-8 strings, so what you store is readable from redis-cli. Plain RedisTemplate defaults to JdkSerializationRedisSerializer, which writes Java binary and requires every stored class to implement Serializable. Most applications want string keys with a JSON value serializer, which means configuring a template rather than using either default.
Does Spring Boot Use Lettuce or Jedis?
Lettuce, by default. spring-boot-starter-data-redis brings in Lettuce, and LettuceConnectionFactory is auto-configured unless you change it. To use Jedis, set spring.data.redis.client-type to jedis, add the Jedis dependency and exclude lettuce-core. The choice matters beyond preference: only Lettuce supports the reactive API, only Lettuce supports master/replica reads in Boot's auto-configuration, and the two have very different timeout defaults.
Why Did My Cache Writes Become Asynchronous in Spring Data Redis 4?
Because your connection factory is reactive. In Spring Data Redis 4.0 the default RedisCacheWriter performs put, evict and clear asynchronously when the connection factory implements ReactiveRedisConnectionFactory, and LettuceConnectionFactory does, so a standard Spring Boot application gets asynchronous cache writes with no reactive code of its own. Build the writer with RedisCacheWriter.create(connectionFactory, configurer -> configurer.immediateWrites()) to restore synchronous behaviour.
Next Steps
A Spring Data Redis application with string keys and a JSON value serializer, default typing enabled deliberately rather than inherited, a command timeout set on the driver, and a considered answer to whether cache writes are synchronous will behave predictably in production. None of that is the default, and most of it is configured a layer below the one you read about.
From here, Spring Boot Redis caching covers @Cacheable and the cache-side defaults, and Redis Java clients covers the landscape underneath the abstraction.
If distributed locks, collections or a near cache are on your list, try Redisson PRO free or compare the editions in the feature comparison.