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 at the bottom layer 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 was the last open-source release) | Jackson 2 | The last 3.x line. Free support ended 30 June 2026. |
| 4.0.x | 4.0.x | Jackson 3 | Spring Boot 4.0 shipped 20 November 2025, with free support until 31 December 2026. The breaking changes covered below land here. |
| 4.1.2 | 4.1.2 | Jackson 3 | Current, with free support until 31 July 2027. 4.1 added annotation-driven pub/sub listeners. |
Versions are the latest releases on Maven Central as of 23 October 2026. Pre-releases are left out.
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.2 manages Spring Data BOM 2026.0.2, resolving spring-data-redis to 4.1.2. 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.2 on Spring Boot 4.1.2, with 3.x differences called out where they matter.
RedisTemplate, and the Serializer Decision
RedisTemplate is Spring Data Redis's central class for running Redis commands. It takes connections from the connection factory, serializes keys and values on the way in and out, and exposes one operations view per Redis data type.
The starter gives you two template beans without being asked: a RedisTemplate<Object, Object> and a StringRedisTemplate. Both wrap the same connection factory and differ only in their serializers.
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. Most have a bound* sibling, such as boundValueOps("key"), that takes the key once rather than on every call; opsForHyperLogLog and opsForCluster do not.
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, among others, 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. Redisson's RLock checks the owner on release and renews its lease while the holder is alive, and adding it leaves your RedisTemplate code as it is; the last section shows how.
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. The Redis starter does not bring Jackson with it: web applications usually have it already, and otherwise you add tools.jackson.core:jackson-databind, whose version Spring Boot manages. Then you build the serializer:
@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()
.allowIfSubType("com.example.orders.") // your own stored types
.allowIfSubType((ctx, type) -> type.getName().startsWith("java.util.")
&& (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)))
.allowIfSubType(Number.class) // Long, BigDecimal and the rest inside maps and lists
.build())
.build();
template.setValueSerializer(json);
template.setHashValueSerializer(json);
return template;
}
The validator is the security boundary here. Default typing writes class names into the stored JSON, and on the way back Jackson creates whatever class a type header names, so a validator that admits every class lets anyone who can write to Redis choose what your application instantiates. The rules above admit your own package, the JDK's lists, sets and maps, and the number types, which carry a type id whenever they sit in an Object field, a map or a list. Other classes named in a type header are refused, with one gap you cannot close from here: Spring Data Redis resolves the top-level type itself, and final JDK classes named there are created without consulting the validator. Add allowIfSubTypeIsArray() if you store arrays of your own types, and a rule for Date if it appears. Avoid allowIfBaseType(Object.class), which reads like a restriction but admits every class.
Values the serializer writes without a type id do not survive the trip as the top-level value, whichever validator you choose. Immutable collections such as List.of(...) fail to read back; enums, dates and UUID values come back as strings; a Long that fits in an int comes back as an Integer, and a Float as a Double. Inside your own classes the declared field types carry them through, so wrap such values in a class, copy collections into an ArrayList, or keep plain counters on StringRedisTemplate.
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. Spring calls afterPropertiesSet() for you when the template is a @Bean, as here; call it yourself on a template built outside the container. StringRedisTemplate(RedisConnectionFactory) calls it in its constructor.
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 takes a few more lines and puts 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, and most of the guides ranking for Spring Data Redis, Baeldung's among them, still teach the 3.x serializers.
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, deprecated for removal, so that you can read data the old serializer wrote; they need Jackson 2 on the classpath to do it.
The rename is not a drop-in, and this is the trap
GenericJackson2JsonRedisSerializer enabled default typing on its own: both its no-argument constructor and builder().build() turned it on, with no PolymorphicTypeValidator required. GenericJacksonJsonRedisSerializer has no no-argument constructor, and its builder leaves default typing off until you ask for it.
So a find-and-replace that turns GenericJackson2JsonRedisSerializer.builder().build() into its Jackson 3 equivalent 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: the removal of a deprecated RedisCacheManager constructor that took its last two arguments in the other order, 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" "com.example.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, and Near or Within on a geo index runs GEORADIUS.
The limits worth knowing before you commit
- The query vocabulary is small.
And,Or,Is/Equals,IsTrue,IsFalse,Near/Withinon a@GeoIndexedproperty,Top/First,OrderByand paging, plus Query by Example throughQueryByExampleExecutor. 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. The comparator reads the property through Spring's expression language, so a private field you sort on needs a public getter. - Limits apply before the sort.
Top,Firstand paging cut the unsorted list of matching ids first, and the sort runs on what is left.findTop3ByFirstnameOrderByAgeDesctherefore returns three arbitrary matches in age order, not the three oldest. When the order decides which entities you get, fetch all matches and sort and trim them yourself. - 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. When the expiry listener is on, saving an entity that has a TTL also writes a phantom copy of it, 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. With it off, no phantom copy is written and expired entities leave their index entries behind. Turn it on if you rely on RedisKeyExpiredEvent, or on indexes staying clean as entities expire.
In Spring Boot, turning it on means declaring @EnableRedisRepositories(enableKeyspaceEvents = EnableKeyspaceEvents.ON_STARTUP) yourself, and that switches off Boot's repository auto-configuration. From then on the annotation decides which packages are scanned, starting from the package of the class that carries it, so put it on a class in the root package of your repositories or set basePackages.
ON_STARTUP also has Spring Data Redis read, and if necessary set, the server's notify-keyspace-events setting through CONFIG commands, which many managed services refuse. There, enable keyspace notifications on the server yourself and add keyspaceNotificationsConfigParameter = "" to the annotation so the CONFIG calls are skipped.
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 | Pooled, inside Jedis's RedisClient (Spring Data Redis 4.1+ with Jedis 7.3+) |
| Pool needed? | Usually not | Always; the driver pools internally |
Implements ReactiveRedisConnectionFactory | Yes | No |
| Default timeouts | 60 s per command, 10 s to connect | 2000 ms connect, 2000 ms read |
| Master/replica reads in Boot auto-config | Yes | No |
| Version Spring Boot 4.1 manages | 7.5.2 | 7.4.1 |
Switching takes a dependency and a property: put Jedis on the classpath and set spring.data.redis.client-type to jedis. With the property set, Lettuce can stay on the classpath; without it, Boot picks Lettuce whenever both are present. The property arrived in Spring Boot 2.4 as spring.redis.client-type, took its current name in 3.0, and accepts exactly two values.
spring:
data:
redis:
client-type: jedis # or lettuce, the default
For driver settings that have no Spring Boot property, declare a customizer bean such as LettuceClientConfigurationBuilderCustomizer or LettuceClientOptionsBuilderCustomizer, and Boot applies it to the factory it builds. You can also build the factory yourself, but then none of the spring.data.redis.* connection properties apply:
@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. Lettuce's command timeout defaults to 60 seconds, and a request thread blocked on Redis serves nothing else; spring.data.redis.timeout shortens it. Its request queue is unbounded unless you bound it, so an outage becomes a heap problem rather than a fast failure, and that setting has no property. A customizer bean sets it without giving up Boot's factory:
@Bean
LettuceClientOptionsBuilderCustomizer boundedRequestQueue() {
return options -> options.requestQueueSize(10_000);
}
Both settings are covered 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, and so does Redisson's factory. JedisConnectionFactory does not. Lettuce is the default.
So a stock application with spring-boot-starter-data-redis, spring-boot-starter-cache (which Boot 4 needs for cache auto-configuration) and @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:
@Bean
RedisCacheManagerBuilderCustomizer immediateCacheWrites(RedisConnectionFactory connectionFactory) {
return builder -> builder.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 two layers below the annotation.
The pool properties and customizer beans in this section belong to the drivers, so they do not reach 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. The asynchronous cache-write default applies to it too, whenever RedisCacheManager is the cache manager in use.
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.
To find every old key at once, add spring-boot-properties-migrator to the build for one run. At startup it lists each spring.redis.* key it finds, with the spring.data.redis.* name to use where there is one, and maps the renamed keys over for that run. Remove it once the keys are renamed.
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
Three behaviours in there that are not obvious from the key names:
urlwins. Setspring.data.redis.urlandhost,port,database,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, and settingspring.data.redis.lettuce.pool.enabled=truewithout it fails at startup. On Lettuce a pool 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
Spring Boot 4 modularised auto-configuration, and the Redis classes moved and changed name while the property prefix stayed put:
| 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.
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. When neither spring.redis.redisson.config nor spring.redis.redisson.file is set, the Redisson starter builds its configuration from the spring.data.redis.* connection settings instead, including host, port, credentials, database, Sentinel, cluster nodes, timeouts and SSL. The pool and driver-specific keys have nothing to configure there.
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 redisMessageListenerContainer(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(OrderMessageHandler handler) {
return new MessageListenerAdapter(handler, "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. The adapter calls the named method, handleMessage here, on whatever object you hand it.
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 named redisMessageListenerContainer unless a bean with that name already exists:
@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.
Boot looks for the container by bean name only. Declare yours under any other name, such as a @Bean method called container, and Boot still creates its own: you end up with two containers, and every @RedisListener method registers on Boot's rather than on the one you configured. Naming yours redisMessageListenerContainer, as the first example does, makes Boot back off, and the annotated listeners join it.
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, in the org.redisson:redisson-spring-transaction module, which plugs into @EnableTransactionManagement, so @Transactional has something to drive without a JDBC datasource in the application; ReactiveRedissonTransactionManager does the same for WebFlux. Adding Redisson alongside Spring Data Redis is covered at the end of this guide.
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: of the two drivers, 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 drivers rather than to Redis. Redisson's connection factory implements the reactive interface as well, and Redisson itself 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. Set one or the other: with both present, Boot configures Sentinel.
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. The PropertySource constructors are the change most likely to break an existing configuration class: Spring Data Redis 3.3 added static factories and deprecated the constructors, and 4.0 removed them, so you can switch before you upgrade:
// Up to 3.5 (deprecated since 3.3, removed in 4.0)
RedisClusterConfiguration before = new RedisClusterConfiguration(propertySource);
// 3.3 and later, including 4.x
RedisClusterConfiguration after = 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.
The operational half lives in the driver. Topology refresh on Redis Cluster is a driver setting, and Lettuce 7, which Spring Boot 4.1 ships, refreshes adaptively by default: a MOVED or ASK redirect, repeated reconnect failures, an uncovered slot or an unknown node each trigger a fresh read of the topology. Periodic refresh is still off; set spring.data.redis.lettuce.cluster.refresh.period if you want a scheduled refresh as well. Spring Boot 4.0 ships Lettuce 6.8, where the adaptive triggers start off, so on 4.0 set spring.data.redis.lettuce.cluster.refresh.adaptive=true. Older guides tell you to enable the adaptive triggers yourself. On Lettuce 7 that call is deprecated and has no effect, and Boot 4.1 retired its refresh.adaptive property for the same reason. 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 and no near cache, and its collection support stops at maps, lists, sets, sorted sets and atomic counters. For a lock you either add Spring Integration's RedisLockRegistry or write one 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.8.0</version>
</dependency>
Redisson's auto-configuration runs before Spring Boot's and registers its own factory, so Boot's Lettuce factory backs off. Excluding Lettuce as well keeps an unused driver, and the client resources Boot still creates for it, out of the application:
<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 if the holder dies the lock expires once the 30-second watchdog timeout runs out. That is what the hand-rolled version gets wrong.
If the application uses Spring's cache abstraction, also set spring.cache.type=redis. Redisson brings a JCache provider with it, and Spring Boot prefers JCache to Redis when both are available, so without the property @Cacheable quietly moves from RedisCacheManager to a JCacheCacheManager.
Beyond locks, this is what Redisson adds and which edition each part is in:
| 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; DefaultRedisMap, DefaultRedisList, DefaultRedisSet, DefaultRedisZSet | RMap implements ConcurrentMap, RList, RSet, RQueue — 60+ objects | Open-source |
| Per-entry TTL and max-idle | Per-key TTL, and per-field TTL on hashes since 3.5; no max-idle | RMapCache, RMapCacheNative | Open-source |
| Near cache | — | RLocalCachedMap | Open-source |
| Spring Cache manager | RedisCacheManager | RedissonSpringCacheManager (redisson-spring-cache module), 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 manager | — | RedissonClusteredSpringCacheManager | PRO |
The 4.8.0 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, add the Jedis dependency and set spring.data.redis.client-type to jedis; with the property set, lettuce-core can stay on the classpath. The choice matters beyond preference: of the two, only Lettuce supports the reactive API and master/replica reads in Boot's auto-configuration, and the timeout defaults differ, at 60 seconds per command on Lettuce against 2 seconds on Jedis.
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. LettuceConnectionFactory does, and so does Redisson's, 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()) and hand it to the cache manager, for example through a RedisCacheManagerBuilderCustomizer, to restore synchronous behaviour.
Why Are My spring.redis Properties Ignored?
Because Spring Boot 3.0 moved them to spring.data.redis. Keys such as spring.redis.host and spring.redis.password are no longer read, so the application quietly falls back to localhost:6379 with no password. Rename them to spring.data.redis.host and so on, or add spring-boot-properties-migrator for one run to list every old key with its replacement. Spring Boot 4 did not move the prefix again; it renamed the class that reads it, from RedisProperties to DataRedisProperties.
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.
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.