⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@Ilyass-Bougati
Copy link

Relates to #1004

Purpose

Following the discussion in #1004 and the suggestion by @onobc, this PR introduces the high-level architectural skeleton for the annotation-driven listener model.

The goal is to align on the core components and interfaces before filling in the implementation logic.

Design Overview

To ensure consistency with the wider Spring ecosystem, this design strictly mirrors the Listener Container Factory pattern found in spring-jms and spring-amqp. This approach decouples the listener model from the execution, allowing for granular configuration (thread pools, serializers) per listener.

Introduced Components:

  • RedisListenerContainerFactory: Strategy interface for creating RedisMessageListenerContainer instances.
  • RedisListenerEndpoint: The model describing a listener configuration.
  • RedisListenerEndpointRegistry: Lifecycle manager for the created containers.
  • RedisListenerAnnotationBeanPostProcessor: Infrastructure bean that detects @RedisListener methods and registers endpoints.
  • @EnableRedisListeners: Configuration annotation to activate the post-processor.

Note on Testing

Per the discussion in #1004, this is a high-level design proposal containing only interfaces and skeleton classes. No test cases are included in this PR as there is no implementation logic yet. Comprehensive unit and integration tests will be added in the subsequent implementation PRs.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 23, 2026
@onobc onobc self-assigned this Jan 23, 2026
@onobc onobc self-requested a review January 23, 2026 20:28
@mp911de
Copy link
Member

mp911de commented Jan 26, 2026

Can you sketch out the declaration side of this change (i.e. how would application code look like) so we have an idea how this is going to be used? With such an approach, we can get an idea which components we need to build and how to approach certain aspects of invocation, exception handling and return value handling.

@mp911de mp911de added status: pending-design-work Needs design work before any code can be developed type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 26, 2026
@Ilyass-Bougati
Copy link
Author

Sure, here is the sketch of the declaration side.

Configuration

The user enables the infrastructure and defines a RedisListenerContainerFactory. This factory is the central place to configure serialization, thread pools, and error handling.

@Configuration
@EnableRedisListeners
public class RedisListenerConfig {

    @Bean
    public RedisListenerContainerFactory<?> redisListenerContainerFactory(RedisConnectionFactory connectionFactory) {
        DefaultRedisListenerContainerFactory factory = new DefaultRedisListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        
        // Custom Serialization
        factory.setObjectMapper(new ObjectMapper()); 
        
        // Global Error Handling (e.g. logging or dead-lettering)
        factory.setErrorHandler(t -> System.err.println("Listener error: " + t.getMessage()));
        
        return factory;
    }
}

Application Code (The Listener)

The developer can simply annotate methods. The infrastructure handles the argument resolution (converting byte[] to String or POJO) and dispatch.

@Service
public class OrderService {

    // Simple String listener on a specific channel
    @RedisListener(topics = "orders.logs")
    public void logOrder(String logMessage) {
        System.out.println("Received: " + logMessage);
    }

    // POJO listener with automatic deserialization (via Jackson/Converter)
    @RedisListener(topics = "orders.created", containerFactory = "myCustomFactory")
    public void createOrder(OrderEvent event) {
        // 'event' is automatically deserialized from the Redis payload
        orderRepository.save(event);
    }

    // Pattern matching listener
    @RedisListener(topicPatterns = "orders.*")
    public void monitorOrders(Message message) {
        // Access raw Spring Data Redis Message object if needed
        System.out.println("Activity on channel: " + new String(message.getChannel()));
    }
}

Handling Invocation & Exceptions

  • Invocation: The MessagingMessageListenerAdapter will handle the reflection and argument resolution (supporting Message, byte[], String, and POJOs).
  • Exceptions: Exceptions thrown by the listener method are caught by the container. By default, they are logged, but the user can provide a standard org.springframework.util.ErrorHandler to the ContainerFactory to customize this behavior (e.g., sending to a dead letter topic).
  • Return Values: For Pub/Sub, the return type is typically void. However, adopting the MessagingMessageListenerAdapter architecture allows us to support @SendTo in the future if we want to support a Request-Reply style pattern (publishing the return value to another topic).

@mp911de
Copy link
Member

mp911de commented Jan 27, 2026

I'm getting pretty much the impression talking to ChatGPT as the descriptions and comments are a wall of text sumarizing themselves discussing obvious details from the JMS listeners without considering Redis-specifics.

The actually interesting questions are:

  • How should a listener method look like for patterns and for channels
  • Should the same method be used for both? Why if so? Why if not?
  • What argument types can be injected into a method?
  • How are these supposed to be deserialized? How is that going to be controlled?

If you would like us to spend some time collaborating with you, please spend some time and effort. We appreciate community contributions, but we do need them to meet a reasonable baseline for consideration. This means:

  • Understanding the existing code structure and following established patterns
  • Providing clear suggestions that match the feature idea problem space
  • Ensuring the contribution aligns with Spring's design principles

Writing the code is not a problem to be solved, we can throw a few sentences into an LLM ourselves. Listening, understanding, and activiating creativity to address a genuine concern is what matters when developing libraries for a broader audience.

When contributions require significant rework from our side, we have to deprioritize or even reject them in favor of changes that are ready for integration as we have to be mindful of our limited time, and we need to focus it where it has the most impact. If you would like us to spend some time working on this pull request, please spend some time making this a substantial and quality contribution.

@mp911de mp911de added the has: ai-slop An bloated issue that contains low-value AI-generated content. label Jan 27, 2026
@Ilyass-Bougati
Copy link
Author

Point taken. I appreciate the direct feedback regarding the contribution baseline; I intended to align on the high-level API first, but I see now that glossing over the runtime specifics made the proposal feel abstract.

To answer your technical constraints directly:

A single @RedisListener annotation should suffice for both fixed channels and patterns. The container can inspect the annotation to determine whether to issue a SUBSCRIBE or PSUBSCRIBE command, effectively abstracting that complexity away from the user. Consequently, the same method signature should support both use cases; the user typically cares only about the payload, not the underlying subscription mechanism. If they do need metadata, such as the source channel or the pattern matched, they can simply opt to inject the org.springframework.data.redis.connection.Message envelope.

On the data side, we need to support byte[], String, and Message injection out of the box, but POJO support is the primary challenge due to the lack of headers in Redis. Unlike JMS or AMQP, we cannot rely on a Content-Type header to infer serialization. Therefore, smart inference is fragile here. We must rely on strict configuration: a default serializer at the ContainerFactory level for the common case, and an explicit override attribute on the annotation for specific listeners (e.g., handling Protobuf on one topic and JSON on another).

I will pivot the PR to focus entirely on this argument resolution and deserialization strategy, as that is clearly where the complexity lies.

@mp911de
Copy link
Member

mp911de commented Jan 27, 2026

Thanks for your suggestion.

So a single annotation to indicate pattern or channel subscriptions:

  • @RedisListener(topic = {"channel1", "channel-foo"})
  • @RedisListener(topic = "my-pattern*")
  • Other naming ideas (see Redis Streams section at the end): @RedisPubSubListener(topic = "my-pattern*"), @RedisChannelListener(topic = "my-channel"), @RedisPatternListener(topic = "my-pattern*"))?

These could be also mixed and we would define once there are placeholders, we're using Pattern subscriptions.

Injectable argument types:

  • org.springframework.data.redis.connection.Message
  • Simple message body types: byte[] and String
  • org.springframework.data.redis.listener.Topic

I think we should also consider @RedisExceptionHandler (similar to @MessageExceptionHandler) do define an error handler that is valid for exception handling of errors within the same class:

@Controller
public class MyController {

	// ...

	@RedisExceptionHandler
	public void handleException(MyException exception) {
		// ...
	}
}

I think this arrangement is sufficient for the very initial step to get something going and already bears quite some complexity.

Spring Framework provides a @Payload annotation in its messaging module to indicate payload/body details. It would make sense to postpone the serializer issue to a later step so that we don't overwhelm ourselves with too much complexity in the first step. Once we have a good setup for listener registration/de-registration and method invocation then we can evolve from there to a model for serializer selection. It's always easier to have some material for experiments.

Let's take another step back to see the greater picture. Right now, we're discussing Redis Pub/Sub. In that context, @RedisListener is unique and clearly describes its intent. Redis provides with Redis Streams another mechanism of messaging and it would make sense to spend a few cycles how an annotation-driven programming model for Redis Stream consumption could look like. The intent clearly is not to build such support with this pull request, but rather have this distinction in mind for naming components so that a later Redis Stream listener model would not suffer from poor naming.

@mp911de mp911de removed the has: ai-slop An bloated issue that contains low-value AI-generated content. label Jan 27, 2026
@Ilyass-Bougati
Copy link
Author

That sounds like a solid plan. I agree that keeping the scope tight (focusing on the registration and invocation mechanics while sticking to simple types first) is the best way to stabilize the core before tackling the serialization ambiguity.

Regarding the naming, I think sticking with @RedisListener for Pub/Sub is the right call, especially to mirror @JmsListener. Redis Streams are distinct enough (with consumer groups, offsets, and acks) that they'll likely warrant a dedicated @RedisStreamListener down the road to handle those specific attributes without cluttering the Pub/Sub contract. That said, if you think the distinction needs to be explicit right now to prevent future ambiguity, I’m happy to refactor it to @RedisPubSubListener.

The addition of @RedisExceptionHandler also makes perfect sense and aligns well with the standard Spring method-handling patterns for local error resolution. I'll update the PR to implement the RedisListenerAnnotationBeanPostProcessor and the MessagingMessageListenerAdapter with support for byte[], String, and Message types, leaving the complex content negotiation for the next iteration.

Ilyass-Bougati and others added 6 commits January 28, 2026 17:09
Change validation from checking nanosecond component to comparing
the entire Duration against Duration.ofSeconds(1).

This allows Duration values like 1.5 seconds or Duration.ofMillis(1001)
to be correctly accepted.

Closes spring-projects#2975
Original Pull Request spring-projects#3302

Signed-off-by: CHANHAN <[email protected]>
Signed-off-by: Ilyass-Bougati <[email protected]>
- Move timeout related tests into parameterized unit test.
- Move isZeroOrGreaterOneSecond into TimeoutUtils and simplify the tests.

See spring-projects#2975
Original Pull Request spring-projects#3302

Signed-off-by: Chris Bono <[email protected]>
Signed-off-by: Ilyass-Bougati <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: pending-design-work Needs design work before any code can be developed type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants