JMS Messaging Over Valkey and Redis

Published on
April 30, 2026

Today's enterprise applications are built upon many separate components. In modern architecture, developers must craft clear interfaces between components to enable reliable communication and data exchange. Message queues provide an elegant solution and, for Java developers, the Java Message Service (JMS) is the standard messaging API.

JMS, now officially known as Jakarta Messaging, is widely used because it provides a unified Java interface for working with popular message brokers, such as IBM MQ, as well as other enterprise-grade services. These high-end message brokers read and write fast, and offer varying levels of control over message queues, yet add significant weight to a tech stack.

Meanwhile, the fast in-memory data stores Valkey and Redis can serve as message brokers perfectly well. The only problem is the lack of native Java support in Valkey or Redis. Redisson PRO, the premier Valkey/Redis client for Java, bridges this gap with its JMS API implementation. Here's an overview of how Redisson PRO delivers JMS messaging backed by Valkey or Redis.

Redisson PRO's JMS API: An Architectural Overview

The Redisson PRO JMS API is architecturally sound and passed all standard TCK tests. It supports the current standard, Jakarta Messaging 3.1, as well as the older specs JMS 2.0 and JMS 3.0.

More importantly for Java developers, Redisson PRO maps JMS concepts directly to native Valkey or Redis data structures. Consider the point-to-point messaging scheme. For this, JMS uses queues. Redisson maps these to its Reliable Queue feature via its RReliableQueue object. Unlike some high-end message brokers, Reliable Queue guarantees safe delivery.

For publish-subscribe (pub/sub) messaging, JMS uses topics. When a publisher sends a message, many subscribers can receive it simultaneously. Redisson's RReliablePubSubTopic provides a full-featured pub/sub implementation.

Getting Started With Redisson PRO's JMS API

To get started with Redisson PRO and its JMS API in your Java development environment, you only need a few things. You need a running instance of Valkey or Redis, either a single server or a cluster. If you're using a cluster, Redisson PRO supports master-slave replication across all server modes.

Then, it's just a matter of adding the right dependency to your project, based on the version of the JMS spec you wish to use. How you accomplish this depends on your Java development environment.

First, let's look at Maven and what you need to add to your pom.xml file for various JMS versions:

For JMS 2.0:

<dependency>
    <groupId>pro.redisson</groupId>
    <artifactId>redisson-jms-20</artifactId>
    <version>xVERSIONx</version>
</dependency>

For JMS 3.0:

<dependency>
    <groupId>pro.redisson</groupId>
    <artifactId>redisson-jms-30</artifactId>
    <version>xVERSIONx</version>
</dependency>

For JMS 3.1:

<dependency>
    <groupId>pro.redisson</groupId>
    <artifactId>redisson-jms-31</artifactId>
    <version>xVERSIONx</version>
</dependency>

If you use Gradle, you must compile the library in your build script. This only takes one extra line in the script:

// JMS 2.0 
compile 'pro.redisson:redisson-jms-20:xVERSIONx'

// JMS 3.0 
compile 'pro.redisson:redisson-jms-30:xVERSIONx'

// JMS 3.1 
compile 'pro.redisson:redisson-jms-31:xVERSIONx'

Once you resolve your dependencies, you're ready to utilize Redisson PRO's JMS API in your Java code.

Working With the Redisson PRO JMS API

To access the Redisson PRO JMS API, everything starts with the RedissonConnectionFactory object. It only takes a few lines of code to create a new ConnectionFactory and its configuration:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.jms.RedissonConnectionFactory;

Config config = new Config();
config.useSingleServer()
      .setAddress("redis://127.0.0.1:6379")
      .setTimeout(5000);
config.setThreads(16);

RedissonClient redisson = Redisson.create(config);
ConnectionFactory cf = new RedissonConnectionFactory(redisson);

As you can see in this code sample, we only set a few defaults in the config — the Valkey or Redis server's IP, a timeout value, and the number of threads. Think of these as default factory-level settings for your message queues and topics.

You can also override these defaults for specific destinations. In this example, we set some defaults for all queues, and define a value specifically for an orders queue:

RedissonConnectionFactory cf = new RedissonConnectionFactory(redisson);

cf.setClientId("my-client-id");
cf.setUser("admin");
cf.setPassword("secret");

// Default for all queues
cf.setQueueConfig(myDefaultQueueConfig);

// Override for a specific queue
cf.setQueueConfig("orders", ordersQueueConfig);
Sending and Receiving Messages

Sending and receiving messages is a simple matter of creating a queue and building a producer. Next, you set a delay. Then, you send the payload and create a consumer. In the last bit of code in this sample, you read the payload:

try (JMSContext ctx = cf.createContext()) {
    Queue queue = ctx.createQueue("orders");

    ctx.createProducer()
       .setDeliveryDelay(1000)
       .setPriority(7)
       .send(queue, "order-456");

    String body = ctx.createConsumer(queue)
                     .receiveBody(String.class, 5000);
}

Pub/sub messaging works similarly. One advantage of Redisson PRO is the ability to create durable subscriptions. These subscriptions persist across client restarts, so if your application crashes, the subscription survives — a feature not found even in some high-end message brokers.

Here's how to get started with pub/sub messaging and durable subscriptions:

try (JMSContext ctx = cf.createContext()) {
    Topic topic = ctx.createTopic("events");
    JMSConsumer subscriber = ctx.createDurableConsumer(topic, "my-durable-sub");

    ctx.createProducer().send(topic, "event-1");

    String event = subscriber.receiveBody(String.class, 5000);
}

Spring JMS Integration: Step-By-Step

The Spring framework contains the spring-messaging module, a popular choice for building enterprise-class, message-driven applications. Redisson PRO's JMS API integrates with Spring via the RedissonConnectionFactory. It supports all standard Spring interfaces, including JmsTemplate and @JmsListener.

You include your Spring setup strings in a simple Java config. In this code sample, we register the factory as a bean, then use the @EnableJms annotation. Spring handles the rest by configuring the template and automatically building the listener container:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.jms.RedissonConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import jakarta.jms.ConnectionFactory;

@Configuration
@EnableJms
public class JmsConfig {

    @Bean
    public ConnectionFactory connectionFactory(RedissonClient redisson) {
        RedissonConnectionFactory cf = new RedissonConnectionFactory(redisson);
        cf.setClientId("my-app");
        return cf;
    }
}

By default, Spring targets queues for point-to-point delivery. If you want to use topics, you must change the config by setting the pubSubDomain property to true.

Here's a sample config:

@Bean
public JmsTemplate jmsTopicTemplate(ConnectionFactory connectionFactory) {
    JmsTemplate template = new JmsTemplate(connectionFactory);
    template.setPubSubDomain(true);
    return template;
}

Here, we configure a separate listener for receiving from topics:

@Bean
public DefaultJmsListenerContainerFactory topicListenerFactory(
        ConnectionFactory connectionFactory) {
    DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setPubSubDomain(true);
    factory.setClientId("my-app");
    return factory;
}

Next, you bind the listener to the topic:

@JmsListener(destination = "events", containerFactory = "topicListenerFactory", subscription = "auditSub")
public void handleEvent(String event) {
    // Process the event
}

Spring Boot makes this even easier. Define the RedissonConnectionFactory bean, and then Boot automatically detects and creates the template. It applies your application.properties settings, such as:

spring.jms.pub-sub-domain=false
spring.jms.listener.concurrency=3
spring.jms.listener.max-concurrency=10

How-To: JNDI Configuration

The Java Naming and Directory Interface (JNDI) is a standard API for finding data and objects by name. Highly centralized and decoupled applications often use JNDI to look up queues, topics, and connection factories by name. As such, JNDI doesn't rely on code to make connections. Instead, you define environment properties.

After you define these properties, the RedissonInitialContextFactory reads them and builds the objects. You set up the environment with a hash table, like this:

Hashtable env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "org.redisson.jms.jndi.RedissonInitialContextFactory");

// Connection factory
env.put("connectionfactory.myFactory.singleServerConfig.address", "redis://127.0.0.1:6379");
env.put("connectionfactory.myFactory.clientId", "myClient");

// Destinations
env.put("queue.myQueue.name", "orders");
env.put("topic.myTopic.name", "notifications");

Context ctx = new InitialContext(env);

ConnectionFactory cf = (ConnectionFactory) ctx.lookup("myFactory");
Queue queue = (Queue) ctx.lookup("myQueue");
Topic topic = (Topic) ctx.lookup("myTopic");

Here, we see that the key connectionfactory.myFactory creates a factory named myFactory. The presence of this property is what creates the object. The same logic applies to dynamic destinations.

Just keep in mind how the JNDI environment lifecycle works. When you close a JNDI context, it shuts down the underlying Redisson clients and releases the background threads. It drops the Redis connections in the process, so you can't use a connection factory after you close its context.

Context ctx = new InitialContext(env);
try {
    ConnectionFactory cf = (ConnectionFactory) ctx.lookup("myFactory");
    // Work with the factory
} finally {
    ctx.close(); // Clients shut down here
}

Configuring Queues, Subscriptions, Topics, and More

Redisson PRO makes it easy to configure all of your queues, topics, and subscriptions. It's also highly flexible, as you can do this in Java code or in JNDI.

However you choose to do it, you can configure the many features of Redisson PRO's RReliableQueue. For example, you can set a deliveryLimit to prevent infinite delivery retries. You can set visibility, which hides a message from other consumers while it's being processed. You can also set a TTL with the timeToLive property, so that old messages expire and vanish according to your app's logic. Here's how to do that in Java code:

QueueConfig queueConfig = QueueConfig.defaults()
    .deliveryLimit(5)
    .visibility(Duration.ofSeconds(60))
    .timeToLive(Duration.ofHours(24))
    .deadLetterQueueName("orders-dlq")
    .maxSize(10000)
    .processingMode(ProcessingMode.SEQUENTIAL);
cf.setQueueConfig("orders", queueConfig);

And in JNDI, you define flat properties:

queue.orders.name=orders
queue.orders.deliveryLimit=5
queue.orders.visibility=60000
queue.orders.timeToLive=PT24H
queue.orders.deadLetterQueueName=orders-dlq
queue.orders.maxSize=10000
queue.orders.processingMode=SEQUENTIAL

Master-slave replication is also configurable. For example, you configure synchronization via JmsSyncConfig. You can set the syncMode to AUTO, ACK, or ACK_AOF. Other handy properties include the syncTimeout and the syncFailureMode. As with other Redisson PRO features, you can configure these settings globally and override the defaults on a per-destination basis.

Things to Consider Before Implementation

The Redisson PRO JMS API is easy to work with, powerful, and flexible. But as with all tools in your application development ecosystem, you can't just install it and figure things out along the way. You need to understand the capabilities, define your queues and topics, and form a plan.

In particular, you should consider Redisson PRO's Reliable Queue and Reliable Pub/Sub, along with the features they offer, some of which might not be found in the message brokers you've used before.

A good example is the message visibility timeouts. You need to carefully apply the appropriate timeout value, as it can significantly impact your app's stability.

In particular, a short timeout can be dangerous. For example, if a consumer is still working on a message when the timeout expires, the message reappears in the queue. Another consumer could grab it, leading to the same message being processed twice.

On the other hand, a long timeout can be equally bad. If a consumer crashes, the message remains hidden and the queue stalls. Match the timeout to your actual, measured processing speed.

You should also take advantage of Redisson PRO features like dead-letter queues. DLQs ensure that bad messages don't loop forever, wasting network bandwidth and computing cycles. By sending failed messages to a DLQ, you can build a process in your app to inspect them and fix the root cause.

Redisson PRO's ability to set message sizes is also important. Valkey and Redis are fast because they store all data in memory, but excessively large messages can undermine that speed. Take advantage of this feature to keep messages appropriately sized for your application's needs.

A Powerful and Compliant JMS API on Valkey and Redis

Valkey and Redis provide exceptional speed and flexibility as message brokers. However, Java developers may not consider Valkey or Redis for that purpose because they lack native Java support. Redisson PRO changes all that by implementing a powerful and fully compliant JMS API, built on top of the Valkey or Redis infrastructure you already own.

To learn more, check out the list of features offered by Redisson PRO.

Similar articles