Spring Boot: Spring Data Redis, Redis OM & Custom Queries

This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Examples on this page target Redis Open Source 8.10 (8.10.2), Spring Boot 4.1, Spring Data Redis 4.1, Redis OM Spring 1.0, Spring Session 4.1, and the Jedis 8 and Lettuce 7.7 client libraries. Spring Data Redis is the general-purpose Spring Data module for Redis — RedisTemplate, opsFor*, and @RedisHash repositories — covered in the module table in Spring Data Overview. Redis OM Spring is a separate, higher-level project layered on top of it that maps @Document entities onto the Query Engine index covered in Query Engine: Indexing & Data Modeling, giving a Spring Data repository real filtering, sorting, and aggregation instead of the derived-finder limits Spring Data Redis’s own repositories have. This page covers both, how they relate, and where an application still needs to drop down to a raw client.

RedisTemplate and StringRedisTemplate

RedisTemplate<K, V> is Spring Data Redis’s low-level access point, the same role MongoTemplate or Neo4jClient play for their own stores in Spring Data Overview: it wraps a RedisConnectionFactory, serializes keys and values through configurable RedisSerializer instances, and exposes the full command surface through a set of opsFor* accessors rather than one flat method list. StringRedisTemplate is a RedisTemplate<String, String> pre-configured with StringRedisSerializer on every slot, the right choice whenever both keys and values are plain strings and no other serializer is needed.

package com.example.bookshop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(RedisSerializer.json());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(RedisSerializer.json());
        return template;
    }
}

Spring Boot’s auto-configuration already provides both beans against spring.data.redis.* properties once spring-boot-starter-data-redis is on the classpath; the bean above is only needed to override the default JDK serializer with a JSON one, the usual choice once values are domain objects rather than plain strings. RedisSerializer.json() returns the Jackson 3 GenericJacksonJsonRedisSerializer. It replaces GenericJackson2JsonRedisSerializer, which Spring Data Redis 4.0 deprecated for removal. Its trade-offs are covered in Serializer choices. This derives from Spring Data Redis: template API.

The opsFor* APIs

Each opsFor* accessor scopes the template to one Redis data type’s commands, mirroring the raw command families the rest of this section documents:

package com.example.bookshop.inventory;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class StockService {

    private final RedisTemplate<String, Object> redisTemplate;

    public StockService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public Long decrementStock(String isbn, long quantity) {
        return redisTemplate.opsForHash()
                .increment("bookshop:bookhash:" + isbn, "stock", -quantity);
    }
}

This derives from Spring Data Redis: template API.

@RedisHash repositories

Spring Data Redis’s repository abstraction persists a Java type as a hash, one field per property, the same programming model Spring Data Overview describes for JPA/MongoDB/Couchbase/ Neo4j repositories — but with a much narrower query surface, described below.

package com.example.bookshop.catalog;

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;
import org.springframework.data.redis.core.index.Indexed;

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.TimeUnit;

@RedisHash("book")
public class BookEntity {

    @Id
    private String isbn;

    @Indexed
    private String author;

    private String title;
    private List<String> genres;
    private BigDecimal price;
    private int year;

    @Reference
    private PublisherEntity publisher;

    @TimeToLive(unit = TimeUnit.SECONDS)
    private Long ttlSeconds;

    // getters and setters omitted
}
  • @RedisHash("book") — names the hash-key prefix (book:<id>) and registers the type for repository support; Spring Data Redis additionally maintains a Set of every ID under book for findAll().

  • @Id — the property mapped to the key’s ID segment; generated automatically (as a String UUID) if left unset.

  • @Indexed — adds a secondary index (a Redis Set per distinct value) for that property, the only thing that makes a property queryable by a derived finder — see below.

  • @TimeToLive — expires the whole hash automatically, the same EXPIRE semantics Architecture, Keyspace & Eviction covers at the command level, applied declaratively per entity instead of a separate EXPIRE call.

  • @Reference — stores only the referenced entity’s key and resolves it lazily on read, rather than embedding the referenced hash’s fields inline.

package com.example.bookshop.catalog;

import org.springframework.data.repository.CrudRepository;

import java.util.List;

public interface BookRepository extends CrudRepository<BookEntity, String> {

    List<BookEntity> findByAuthor(String author);
}

This derives from Spring Data Redis: repositories.

The limits of derived finders

findByAuthor above works only because author carries @Indexed; Spring Data Redis has no query engine of its own to fall back on, so a derived finder against a non-indexed property, a range condition (findByYear GreaterThan), sorting, or a @Query with a custom filter expression are all unsupported — every derived method compiles, but only exact-match lookups against @Indexed properties actually work at runtime. This is the sharpest contrast with the query and aggregation surface Query Engine: Search & Aggregations covers for a Query Engine index: a @RedisHash repository’s indexes are plain Redis Sets built and maintained by Spring Data itself, not FT.CREATE schema fields, so there is no TEXT relevance ranking, no NUMERIC range query, and no aggregation pipeline behind it. An application that needs any of that — full-text search, range filters, sorting by a non-ID field, vector similarity — reaches for Redis OM Spring instead, covered below, which maps entities onto an actual Query Engine index rather than plain indexed Sets.

SessionCallback, executePipelined, and @Transactional

RedisTemplate exposes three ways to run more than one command as a unit, mirroring the raw primitives Pipelining, Transactions & Scripting covers at the protocol level:

  • execute(SessionCallback<T> action) — runs the callback against a single, dedicated connection, so a hand-written MULTI/EXEC block (or a WATCH-based optimistic-locking retry, the same pattern that page builds in Lettuce) sees a consistent connection throughout instead of the template borrowing one connection per call. See Spring Data Redis: transactions.

  • executePipelined(RedisCallback<?> action) — runs the callback’s commands as a client-side pipeline (the same transport-level batching that page’s pipelining section describes) and returns the collected replies as a List<Object>, in call order.

  • setEnableTransactionSupport(true) plus @Transactional — binds the template to Spring’s ordinary declarative-transaction machinery: a @Transactional method’s Redis writes are queued via MULTI and only EXEC’d if the method returns normally, mirroring a JPA `@Transactional method’s commit/rollback shape even though Redis transactions themselves have no rollback, as that page explains.

package com.example.bookshop.inventory;

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderService {

    private final RedisTemplate<String, Object> redisTemplate;

    public OrderService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public List<Object> reserveStock(String isbn, long quantity) {
        return redisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            @SuppressWarnings("unchecked")
            public List<Object> execute(org.springframework.data.redis.core.RedisOperations operations) {
                operations.multi();
                operations.opsForHash().increment("bookshop:bookhash:" + isbn, "stock", -quantity);
                operations.opsForValue().increment("bookshop:stats:orders_total");
                return operations.exec();
            }
        });
    }

    public List<Object> bulkLoad(List<String> isbns) {
        return redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (String isbn : isbns) {
                connection.hashCommands().hIncrBy(
                        ("bookshop:bookhash:" + isbn).getBytes(), "stock".getBytes(), 0);
            }
            return null;
        });
    }
}

Custom queries: Lua, Functions, Pub/Sub, Streams, and keyspace events

RedisScript and classpath-resource scripts

RedisScript<T> wraps a Lua script’s source and its expected return type for RedisTemplate.execute; the Lua syntax itself, KEYS/ARGV, and redis.call/redis.pcall semantics are covered in Pipelining, Transactions & Scripting and not repeated here. The usual pattern loads the script body from a classpath resource rather than an inline Java string, so a .lua file stays syntax- highlighted and diffable on its own:

package com.example.bookshop.inventory;

import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ReservationService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisScript<Long> reserveStockScript;

    public ReservationService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.reserveStockScript = RedisScript.of(
                new ClassPathResource("scripts/reserve-stock.lua"), Long.class);
    }

    public Long reserve(String isbn, long quantity) {
        return redisTemplate.execute(reserveStockScript,
                List.of("bookshop:bookhash:" + isbn), String.valueOf(quantity));
    }
}

redisTemplate.execute transparently uses EVALSHA once the script is cached server-side and falls back to EVAL on a NOSCRIPT reply, the same fallback that page describes at the command level, without application code handling it explicitly. FCALL-invoked Functions have no dedicated Spring Data Redis wrapper and go through the same execute(RedisCallback<T>)/low-level connection API the pipelining example above uses. This derives from Spring Data Redis: scripting.

Pub/Sub with RedisMessageListenerContainer

RedisMessageListenerContainer manages the dedicated subscriber connection Pub/Sub needs (covered at the protocol level in Streams, Pub/Sub & Keyspace Notifications) and dispatches incoming messages to registered MessageListener beans, rather than an application managing a raw subscribed connection and its own listener thread.

package com.example.bookshop.cache;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class InvalidationListenerConfig {

    @Bean
    public RedisMessageListenerContainer invalidationListenerContainer(
            RedisConnectionFactory connectionFactory, CacheInvalidationListener listener) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(
                new MessageListenerAdapter(listener, "onMessage"),
                new ChannelTopic("bookshop:invalidate"));
        return container;
    }
}

Streams with StreamMessageListenerContainer

StreamMessageListenerContainer is the continuous-consumption counterpart to opsForStream(): it polls XREADGROUP in a background thread (or reactively) and dispatches each delivered record to a StreamListener, taking care of the read loop the raw XREADGROUP examples in Streams, Pub/Sub & Keyspace Notifications otherwise write by hand. The Bookshop fulfilment workers, worker-1 and worker-2, both read from the fulfilment consumer group on bookshop:orders:

package com.example.bookshop.fulfilment;

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamMessageListenerContainerOptions;

import java.time.Duration;

public class FulfilmentWorkerConfig {

    public StreamMessageListenerContainer<String, MapRecord<String, String, String>> fulfilmentContainer(
            RedisConnectionFactory connectionFactory, StringRedisTemplate redisTemplate) {

        StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
                StreamMessageListenerContainerOptions.builder()
                        .pollTimeout(Duration.ofSeconds(2))
                        .build();

        StreamMessageListenerContainer<String, MapRecord<String, String, String>> container =
                StreamMessageListenerContainer.create(connectionFactory, options);

        container.receive(
                Consumer.from("fulfilment", "worker-1"),
                StreamOffset.create("bookshop:orders", org.springframework.data.redis.connection.stream.ReadOffset.lastConsumed()),
                message -> {
                    fulfilOrder(message.getValue());
                    redisTemplate.opsForStream().acknowledge("bookshop:orders", "fulfilment", message.getId());
                });

        container.start();
        return container;
    }

    private void fulfilOrder(java.util.Map<String, String> orderFields) {
        // process order_id / isbn / qty
    }
}

worker-2 registers a second receive(…​) call against Consumer.from("fulfilment", "worker-2") on the same container, so both consumers share the group’s work as the diagram below shows. Acknowledgment is left to the listener itself (as above) rather than left to StreamMessageListenerContainer’s optional auto-ack mode, since the Bookshop worker only wants an entry removed from the PEL once `fulfilOrder has genuinely completed. This derives from Spring Data Redis: Streams.

Keyspace-event listeners

A keyspace-event listener is an ordinary RedisMessageListenerContainer subscription against the keyevent@<db>:* pattern channels Streams, Pub/Sub & Keyspace Notifications describes, with notify-keyspace-events configured the same way:

package com.example.bookshop.cache;

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

@Component
public class ExpiredKeyListener implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        // evict any local (near-cache) copy tied to expiredKey
    }
}

Registered against the pattern topic keyevent@0:expired on the same RedisMessageListenerContainer shown above, via container.addMessageListener(listener, new PatternTopic("keyevent@0:expired")). This derives from Spring Data Redis: Pub/Sub messaging and Redis keyspace notifications.

ReactiveRedisTemplate

ReactiveRedisTemplate<K, V> mirrors RedisTemplate’s `opsFor* shape over Mono/Flux instead of blocking calls, backed by Lettuce’s native reactive command support — Jedis has no reactive driver, so a reactive Bookshop path needs Lettuce as its connection factory. Near-Far Caches already builds and uses one in depth for a reactive near/far cache; this page only notes it as the reactive counterpart of everything above rather than repeating that setup.

package com.example.bookshop.catalog;

import org.springframework.data.redis.core.ReactiveRedisTemplate;
import reactor.core.publisher.Mono;

public class ReactiveStockService {

    private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;

    public ReactiveStockService(ReactiveRedisTemplate<String, String> reactiveRedisTemplate) {
        this.reactiveRedisTemplate = reactiveRedisTemplate;
    }

    public Mono<Long> decrementStock(String isbn, long quantity) {
        return reactiveRedisTemplate.opsForHash()
                .increment("bookshop:bookhash:" + isbn, "stock", -quantity);
    }
}

Redis OM Spring

Redis OM Spring layers object mapping and a real Query Engine-backed repository on top of Spring Data Redis, closing the derived-finder gap noted above by building and maintaining an FT.CREATE index (Query Engine: Indexing & Data Modeling) from annotated entity classes, instead of plain Redis Sets.

@Document and field annotations

package com.example.bookshop.catalog;

import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.Searchable;
import com.redis.om.spring.annotations.TagIndexed;
import com.redis.om.spring.annotations.NumericIndexed;
import com.redis.om.spring.annotations.GeoIndexed;
import org.springframework.data.annotation.Id;

import java.math.BigDecimal;
import java.util.List;

@Document
public record Book(
        @Id String isbn,
        @Searchable String title,
        @Searchable String author,
        @TagIndexed List<String> genres,
        @NumericIndexed BigDecimal price,
        @NumericIndexed int year,
        @GeoIndexed String warehouseLocation) {
}
  • @Document — maps the type onto a Query Engine index over JSON documents, the JSON-backed indexing style Query Engine: Indexing & Data Modeling documents at the FT.CREATE level.

  • @Indexed — the general-purpose annotation for an exact-match indexed field, roughly Redis OM Spring’s counterpart to a plain TAG/NUMERIC field.

  • @Searchable — a full-text TEXT field, tokenized and scored the way title/author are on the idx:books schema.

  • @TagIndexed — an explicit TAG field, the annotation genres above uses to match the plural, multi-valued genres field on that same schema.

  • @NumericIndexed — a NUMERIC field, usable for range queries and sorting.

  • @GeoIndexed — a GEO field, matching warehouse_location on that schema.

Redis OM Spring 1.0’s exact annotation set and package names should be checked against its current reference docs before relying on them in a new project; the names above are its well-known, long-standing annotation vocabulary.

RedisDocumentRepository, @Query, @Aggregation, and EntityStream

package com.example.bookshop.catalog;

import com.redis.om.spring.annotations.Aggregation;
import com.redis.om.spring.annotations.Query;
import com.redis.om.spring.repository.RedisDocumentRepository;

import java.util.List;

public interface BookDocumentRepository extends RedisDocumentRepository<Book, String> {

    List<Book> findByGenres(String genre);

    @Query("(@genres:{$genre})=>[KNN 5 @embedding $vec AS score]")
    List<Book> findSimilarInGenre(String genre, byte[] vec);

    @Aggregation("FT.AGGREGATE idx:books * GROUPBY 1 @genres REDUCE COUNT 0 AS book_count")
    List<Object> countByGenre();
}

findByGenres is an ordinary derived finder, but — unlike the @RedisHash case above — it works against any field the @Document schema indexes, with the full range/sort/pagination vocabulary Spring Data’s derived finders support elsewhere, because it compiles down to a real FT.SEARCH query rather than a Set lookup. @Query drops to a raw query string for anything a derived finder can’t express, including the KNN vector syntax Vector Search: Query Engine & Vector Sets covers; @Aggregation does the same for the FT.AGGREGATE pipeline Query Engine: Search & Aggregations covers.

EntityStream is Redis OM Spring’s fluent, type-safe alternative to writing those query strings by hand, generating FT.SEARCH calls from a Java Stream-like pipeline over metamodel fields instead:

package com.example.bookshop.catalog;

import com.redis.om.spring.search.stream.EntityStream;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;

@Service
public class BookSearchService {

    private final EntityStream entityStream;

    public BookSearchService(EntityStream entityStream) {
        this.entityStream = entityStream;
    }

    public List<Book> cheapFictionBooks(BigDecimal maxPrice) {
        return entityStream.of(Book.class)
                .filter(Book$.GENRES.eq("fiction"))
                .filter(Book$.PRICE.le(maxPrice))
                .sorted(Book$.PRICE)
                .collect(java.util.stream.Collectors.toList());
    }
}

Book$ is a metamodel class Redis OM Spring generates at build time (one static field per @Document property), the same annotation-processor pattern the Query DSL/JPA metamodel world uses elsewhere in this site’s Spring Data Overview. This section derives from Redis OM Spring.

Dropping down to UnifiedJedis for raw FT./JSON./TS.*

Not every Query Engine or JSON/Time Series feature has a Redis OM Spring wrapper — an uncommon FT.CREATE option, a TS.* command (Probabilistic Structures & Time Series), or a brand-new command a given Redis OM Spring release hasn’t caught up to yet. UnifiedJedis is the base type of the Jedis 8 RedisClient that Vector Search: Query Engine & Vector Sets and Query Engine: Indexing & Data Modeling use directly. It is available as an injectable bean and covers the raw command surface with no abstraction between the call and the wire protocol:

package com.example.bookshop.search;

import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.search.Query;
import redis.clients.jedis.search.SearchResult;
import org.springframework.stereotype.Service;

@Service
public class RawQueryEngineService {

    private final UnifiedJedis jedis;

    public RawQueryEngineService(UnifiedJedis jedis) {
        this.jedis = jedis;
    }

    public SearchResult rawSearch(String queryString) {
        return jedis.ftSearch("idx:books", new Query(queryString));
    }
}

This is the same escape hatch Vector Search: Query Engine & Vector Sets uses for FT.SEARCH/KNN calls that have no higher-level wrapper; Redis OM Spring auto-configures a UnifiedJedis bean alongside its own repositories precisely so an application never has to stand up a second, unmanaged client just to reach a command it hasn’t wrapped yet.

Spring Session Data Redis

Spring Session Data Redis backs HttpSession with Redis instead of an in-memory or sticky-session store, the session store option Logout and Session Management references for a BFF deployment that needs to look up and delete a local session by sid/sub on a back-channel logout. The Bookshop storefront keys its sessions under namespace bookshop:session, so a session lands at bookshop:session:sessions:<id>:

spring:
  session:
    store-type: redis
    redis:
      namespace: bookshop:session
      flush-mode: on-save
      save-mode: on-set-attribute
  • namespace — the key prefix Spring Session uses for every session-related key, bookshop:session above, producing bookshop:session:sessions:<id> per session and a companion expiration-tracking key.

  • flush-mode — on-save (the default) writes session changes back to Redis once per request, at the end of the servlet filter chain; immediate writes on every attribute mutation instead, at the cost of more round trips.

  • save-mode — on-set-attribute (the default) tracks only attributes explicitly set on the session for the next save, always re-persists every attribute on each save regardless of whether it changed.

Enabling it needs only @EnableRedisHttpSession (or, with Spring Boot auto-configuration and spring-session-data-redis on the classpath, no explicit annotation at all once store-type: redis is set):

package com.example.bookshop.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@Configuration
@EnableRedisHttpSession(redisNamespace = "bookshop:session")
public class SessionConfig {
}

This derives from Spring Session: Redis.

@DataRedisTest and Testcontainers

@DataRedisTest is Spring Boot’s test slice for Redis: it configures a RedisConnectionFactory and every Spring Data Redis bean the slice needs (RedisTemplate, repositories) without loading the rest of the application context, the same test-slice pattern used elsewhere in Spring Data Overview for other stores. Paired with the Testcontainers Redis module, it runs against a real, disposable Redis server rather than an embedded fake:

package com.example.bookshop.catalog;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.redis.testcontainers.RedisContainer;
import org.testcontainers.utility.DockerImageName;

import static org.assertj.core.api.Assertions.assertThat;

@DataRedisTest
@Testcontainers
class BookRepositoryIT {

    @Container
    @ServiceConnection
    static RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:8.10.2"));

    @Autowired
    private BookRepository bookRepository;

    @Test
    void savesAndFindsByAuthor() {
        BookEntity book = new BookEntity();
        book.setIsbn("9780000000001");
        book.setAuthor("Martin Kleppmann");
        bookRepository.save(book);

        assertThat(bookRepository.findByAuthor("Martin Kleppmann")).hasSize(1);
    }
}

@ServiceConnection wires the container’s connection details into the test context automatically, the same mechanism Spring Boot’s other Testcontainers integrations use, so no manual spring.data.redis.host/port property override is needed. This derives from Spring Boot: auto-configured Data Redis tests and Testcontainers: Redis module.

Stream consumer-group read/ack flow

The diagram below shows StreamMessageListenerContainer’s continuous read loop against `bookshop:orders, group fulfilment, with both worker-1 and worker-2 receiving and acknowledging entries independently:

sequenceDiagram participant Producer as bookshop-app (order placed) participant Redis as Redis (bookshop:orders) participant Container as StreamMessageListenerContainer participant W1 as worker-1 participant W2 as worker-2 Producer->>Redis: XADD bookshop:orders * order_id 1042 isbn ... qty 2 Container->>Redis: XREADGROUP GROUP fulfilment worker-1 STREAMS bookshop:orders > Redis-->>Container: entry 1758800000000-0 Container->>W1: StreamListener.onMessage(entry) W1->>W1: fulfilOrder(...) W1->>Redis: XACK bookshop:orders fulfilment 1758800000000-0 Container->>Redis: XREADGROUP GROUP fulfilment worker-2 STREAMS bookshop:orders > Redis-->>Container: entry 1758800000010-0 Container->>W2: StreamListener.onMessage(entry) W2->>W2: fulfilOrder(...) W2->>Redis: XACK bookshop:orders fulfilment 1758800000010-0