JMS publish/subscribe is the messaging model in which a publisher sends a message to a topic, and the broker delivers a copy of that message to every subscriber of the topic. The publisher does not know who the subscribers are, so we can add a new subscriber without changing the publisher code.
We use publish/subscribe when one event must reach several independent services. For example, when a hotel booking app confirms a booking, the email service sends a confirmation mail and the invoice service creates an invoice from the same message.
The following example uses Jakarta Messaging 3.1 (the current name of JMS) with an ActiveMQ Artemis 2.57.0 broker on Java 25. Two consumers subscribe to the topic bookings, and one send() call reaches both of them.
try (JMSContext context = factory.createContext()) {
Topic topic = context.createTopic("bookings");
JMSConsumer emailService = context.createConsumer(topic);
JMSConsumer invoiceService = context.createConsumer(topic);
context.createProducer().send(topic, "Booking 42 confirmed");
String email = emailService.receiveBody(String.class, 1000); // "Booking 42 confirmed"
String invoice = invoiceService.receiveBody(String.class, 1000); // "Booking 42 confirmed"
}
Notice that both consumers get the same text, and that we create them before we send the message. A consumer that subscribes after the send() call gets nothing.
Next, we set up the broker and the Maven dependencies, and receive messages with a timeout and with a listener. After that, we keep messages for offline subscribers with durable subscriptions, spread the load with shared subscriptions, and filter messages with selectors.
1. How Publish/Subscribe Messaging Works
In JMS, every message goes to a destination, which is either a queue or a topic. A topic keeps one subscription per subscriber. When a message arrives, the broker puts a copy into every subscription that exists at that moment, and each subscriber reads from its own subscription.

By default, a subscription is nondurable, which means it exists only while the subscriber’s consumer is open. A durable subscription stays on the broker while the subscriber is offline, as we will see in section 5.
The other JMS model is point-to-point messaging, where a queue gives each message to only one consumer. The two models differ in who gets a message and when.
| Topic (publish/subscribe) | Queue (point-to-point) | |
|---|---|---|
| Who gets a message | every subscriber | only one consumer |
| Consumer offline when the message is sent | message is lost, unless the subscription is durable | message waits in the queue |
| Typical use | events such as “booking confirmed” | jobs such as “resize this photo” |
| Jakarta Messaging type | jakarta.jms.Topic | jakarta.jms.Queue |
2. Setting Up Jakarta Messaging With ActiveMQ Artemis
Jakarta Messaging is only an API, so we need a broker and its client library. In this article, we use ActiveMQ Artemis, whose artemis-jakarta-client module brings the jakarta.jms-api 3.1.0 jar with it.
<dependency>
<groupId>org.apache.artemis</groupId>
<artifactId>artemis-jakarta-client</artifactId>
<version>2.57.0</version>
</dependency>
Artemis moved to a new Maven groupId. Versions up to 2.57.0 were published under org.apache.activemq, and Maven prints a relocation warning for that old groupId. Code that imports javax.jms belongs to JMS 2.0 and older; Jakarta Messaging 3.x uses the jakarta.jms package.
The client connects to the broker through a ConnectionFactory. A standalone Artemis broker listens on port 61616 by default, which we create with bin/artemis create mybroker and start with mybroker/bin/artemis run from the Artemis install folder.
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
For tests and demos, we can start the broker inside our own JVM instead. With the artemis-jakarta-server dependency (same version) on the classpath, a few lines start an embedded broker on the same port. The configuration turns off persistence and security, so we use it only on a developer machine.
Configuration config = new ConfigurationImpl()
.setPersistenceEnabled(false)
.setSecurityEnabled(false)
.addAcceptorConfiguration("tcp", "tcp://localhost:61616");
EmbeddedActiveMQ broker = new EmbeddedActiveMQ();
broker.setConfiguration(config);
broker.start();
3. Publishing Messages to a Topic
Since JMS 2.0, a JMSContext combines the connection and the session in one object. It implements AutoCloseable, so a try-with-resources block closes the connection even when the code throws an exception.
The method createTopic() only creates an object that holds the topic name; it does not create the topic in the broker. Artemis creates the address on first use, because a new broker has auto-creation turned on by default. The method JMSProducer.send() wraps a String in a TextMessage for us.
try (JMSContext context = factory.createContext()) {
Topic topic = context.createTopic("bookings");
JMSProducer producer = context.createProducer();
producer.send(topic, "Booking 43 confirmed");
producer.send(topic, "Booking 44 confirmed");
}
4. Receiving Messages From a Topic
A subscriber reads messages in one of two ways. It either asks for the next message and waits for it (synchronous receive), or it registers a listener that the client library calls for every message (asynchronous receive).
4.1. Waiting for a Message With a Timeout
The method receiveBody(String.class, 1000) waits up to 1000 milliseconds and either returns the message body or returns null when no message arrives in time. The method receive() without a timeout blocks until a message arrives or the consumer is closed, so we always pass a timeout in code that must not hang.
JMSConsumer consumer = context.createConsumer(topic);
String body = consumer.receiveBody(String.class, 1000); // next message body, or null after 1 second
Message message = consumer.receiveNoWait(); // next message, or null if none is waiting
If the message body cannot be converted to the requested type, receiveBody() throws a MessageFormatRuntimeException. For other message types, such as an ObjectMessage or a MapMessage, we call receive() and read the body with getBody().
4.2. Receiving Messages With a MessageListener
A MessageListener is the usual choice for a service that runs all the time. The client library calls onMessage() on its own thread for every message, so our main thread does not block. In the demo, a CountDownLatch keeps the program running until both messages arrive.
CountDownLatch received = new CountDownLatch(2);
try (JMSContext subscriberContext = factory.createContext();
JMSContext publisherContext = factory.createContext()) {
Topic topic = subscriberContext.createTopic("bookings");
JMSConsumer consumer = subscriberContext.createConsumer(topic);
consumer.setMessageListener(message -> {
try {
String text = message.getBody(String.class);
System.out.println("Email service got: " + text);
} catch (JMSException e) {
System.out.println("Cannot read message: " + e.getMessage());
} finally {
received.countDown();
}
});
JMSProducer producer = publisherContext.createProducer();
producer.send(topic, "Booking 43 confirmed");
producer.send(topic, "Booking 44 confirmed");
boolean done = received.await(5, TimeUnit.SECONDS);
System.out.println("All messages received: " + done);
}
The listener catches the JMSException itself, because an exception thrown from onMessage() does not reach our main thread.
Email service got: Booking 43 confirmed
Email service got: Booking 44 confirmed
All messages received: true
The method setMessageListener() must not be used in a Jakarta EE web or EJB application. In those containers, we receive topic messages with a message-driven bean instead.
5. Durable Subscriptions for Offline Subscribers
A nondurable subscription is deleted when its consumer closes, so a subscriber that is offline misses every message sent in the meantime. Say the invoice service restarts for a deployment, and three bookings are confirmed during that restart. Without a durable subscription, those three bookings never get an invoice.
The following example shows the problem first. The message is sent before any consumer exists, so the late consumer receives null.
context.createProducer().send(topic, "Booking 45 confirmed"); // nobody subscribed yet
JMSConsumer lateConsumer = context.createConsumer(topic);
String missed = lateConsumer.receiveBody(String.class, 1000); // null
A durable subscription keeps collecting messages while the subscriber is offline. The broker identifies an unshared durable subscription by the client ID and the subscription name, so the subscriber must set the same client ID every time it connects. The client ID must be set right after creating the JMSContext, before any other call on it.
// 1. Create the durable subscription once
try (JMSContext context = factory.createContext()) {
context.setClientID("invoice-service");
Topic topic = context.createTopic("bookings");
JMSConsumer consumer = context.createDurableConsumer(topic, "invoices");
consumer.close();
}
// 2. Publish while the invoice service is offline
try (JMSContext context = factory.createContext()) {
Topic topic = context.createTopic("bookings");
context.createProducer().send(topic, "Booking 46 confirmed");
}
// 3. Reconnect with the same client ID and subscription name
try (JMSContext context = factory.createContext()) {
context.setClientID("invoice-service");
Topic topic = context.createTopic("bookings");
JMSConsumer consumer = context.createDurableConsumer(topic, "invoices");
String body = consumer.receiveBody(String.class, 1000); // "Booking 46 confirmed"
// 4. Delete the subscription when it is no longer needed
consumer.close();
context.unsubscribe("invoices");
}
We can see that the reconnected consumer gets the message that was published while it was offline. A durable subscription keeps storing messages until we delete it with unsubscribe(), so a durable subscription that nobody reads from keeps growing on the broker. If the same subscription name and client ID are used with a different topic or selector, the broker deletes the old subscription and creates a new one.
6. Shared Subscriptions for Several Instances of One Subscriber
In production, the email service often runs as two or more instances. With normal subscriptions, every instance would get every message, and the customer would get two emails for one booking. A shared subscription, added in JMS 2.0, prevents the duplicate emails. All consumers that use the same subscription name share one subscription, and the broker gives each message to only one of them.
The following example creates two consumers with the subscription name email-service on two separate contexts and publishes four bookings.
JMSConsumer instance1 = first.createSharedConsumer(topic, "email-service");
JMSConsumer instance2 = second.createSharedConsumer(topic, "email-service");
for (int i = 1; i <= 4; i++) {
publisher.createProducer().send(topic, "Booking " + (50 + i) + " confirmed");
}
instance 1 got: Booking 51 confirmed
instance 2 got: Booking 52 confirmed
instance 1 got: Booking 53 confirmed
instance 2 got: Booking 54 confirmed
Each booking reaches only one instance of the email service. Other subscribers of the topic, such as the invoice service, still get all four messages through their own subscriptions. The method createSharedDurableConsumer() combines both features, and unlike createDurableConsumer(), it does not need a client ID.
7. Filtering Messages With a Selector
A message selector is a condition, written in a subset of SQL, that the broker checks against the message headers and properties. Only matching messages reach the consumer. For example, the Pune office of the hotel chain wants only the bookings for Pune.
JMSConsumer puneOffice = context.createConsumer(topic, "city = 'Pune'");
JMSProducer producer = context.createProducer();
producer.setProperty("city", "Delhi").send(topic, "Booking 61 confirmed");
producer.setProperty("city", "Pune").send(topic, "Booking 62 confirmed");
String first = puneOffice.receiveBody(String.class, 1000); // "Booking 62 confirmed"
String second = puneOffice.receiveBody(String.class, 500); // null
A selector works only on headers and properties, never on the message body. So the publisher must copy the values that subscribers filter on into message properties.
8. Delivery Mode and Time to Live
By default, JMSProducer sends messages with DeliveryMode.PERSISTENT, so the broker writes them to disk and keeps them after a broker restart. A NON_PERSISTENT message is faster to send but is lost when the broker stops. The time to live sets how long the broker keeps an undelivered message before it expires.
JMSProducer producer = context.createProducer();
int defaultMode = producer.getDeliveryMode(); // 2 (DeliveryMode.PERSISTENT)
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT)
.setTimeToLive(60_000) // expires after 60 seconds
.send(topic, "Room 12 is free again");
A room availability update fits NON_PERSISTENT with a short time to live, because a newer update replaces it soon. A booking confirmation must stay PERSISTENT. Persistence also matters for durable subscriptions, because a nonpersistent message in a durable subscription is lost if the broker restarts.
9. The Classic JMS API in Older Code
Many existing projects and older tutorials use the classic API from JMS 1.1, with a separate Connection, Session, MessageProducer and MessageConsumer. The classic API still works in Jakarta Messaging 3.1. One difference often causes bugs. A classic Connection delivers no messages until we call connection.start(), whereas a JMSContext starts the connection when we create the first consumer.
try (Connection connection = factory.createConnection()) {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic("bookings");
MessageConsumer subscriber = session.createConsumer(topic);
connection.start(); // start delivery to consumers
MessageProducer publisher = session.createProducer(topic);
publisher.send(session.createTextMessage("Booking 42 confirmed"));
TextMessage received = (TextMessage) subscriber.receive(1000); // text "Booking 42 confirmed"
}
The JMS 1.0 interfaces TopicConnection, TopicSession, TopicPublisher and TopicSubscriber still exist too. New code does not need them, because Connection, Session and JMSContext work with both topics and queues.
| Task | Classic API (JMS 1.1) | Simplified API (JMS 2.0+) |
|---|---|---|
| Connect | factory.createConnection() plus createSession() | factory.createContext() |
| Send | session.createProducer(topic).send(message) | context.createProducer().send(topic, “text”) |
| Receive | MessageConsumer.receive(1000) | JMSConsumer.receiveBody(String.class, 1000) |
| Start delivery | connection.start() | on the first consumer |
| Errors | checked JMSException | unchecked JMSRuntimeException |
10. JMS Publish/Subscribe FAQs
10.1. What Is the Difference Between a JMS Topic and a Queue?
A topic gives every message to all its subscribers, whereas a queue gives every message to only one consumer. We publish events that several services react to on a topic, and we put work items that one worker must process on a queue. The table in section 1 compares the two models.
10.2. Does a JMS Topic Keep Messages for Subscribers That Are Offline?
Only with a durable subscription. A nondurable subscriber gets only the messages that arrive while its consumer is open, as the late subscriber in section 5 shows.
10.3. Do Subscribers Get Messages in the Order They Were Sent?
Yes, for the messages that one session sends to the topic, the Jakarta Messaging spec requires that subscribers receive them in the order they were sent. Messages with a higher priority may jump ahead of older messages, though. The spec defines no order between messages from different sessions, and a shared subscription spreads the messages over several consumers that run in parallel.
10.4. How Do I Use JMS Topics in Spring Boot?
We send and receive with Spring’s JmsTemplate and @JmsListener, as in the Spring Boot JmsTemplate with ActiveMQ example. By default, Spring Boot uses queues, so for topics we set the property spring.jms.pub-sub-domain=true.
11. Conclusion
In publish/subscribe messaging, a publisher sends each message to a topic, and every subscription on that topic gets its own copy. With the Jakarta Messaging simplified API, a JMSContext creates the producer and the consumers, and a MessageListener receives messages without blocking our thread.
A normal subscription exists only while its consumer is open, so offline subscribers miss messages. When a subscriber must not miss anything, we use a durable subscription with a fixed client ID and delete it with unsubscribe() when it is no longer needed.
When one subscriber runs as several instances, a shared subscription gives each message to one instance only. Selectors filter messages by their properties, and the delivery mode decides whether a message survives a broker restart. To learn the basic building blocks of JMS, read the JMS tutorial.
12. References
- Jakarta Messaging 3.1 specification
- JMSContext JavaDoc (Jakarta Messaging 3.1)
- JMSConsumer JavaDoc (Jakarta Messaging 3.1)
- Jakarta Messaging Concepts (Jakarta EE Tutorial)
- ActiveMQ Artemis: Using the Server
- ActiveMQ Artemis: Using JMS
Happy Learning !!
suppose I have three client as Receiver clientA, clientB, clientC and so on. clientA is down for 2 hour, once clientA up how should we have guaranty that clientA we receive all messages from last two hour(which was missed in downtime) .
If messages are persistent, its guaranteed.
Make sure you have set the initial context correctly based on your environment.
// Obtain a JNDI connection
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
“weblogic.jndi.WLInitialContextFactory”);
env.put(Context.PROVIDER_URL, “t3://server-name:server-port”);
env.put(Context.SECURITY_PRINCIPAL, “weblogic”);
env.put(Context.SECURITY_CREDENTIALS, “weblogic”);
InitialContext ctx = new InitialContext(env);
Hi Ashish
I have updated my source code but get some issues
Exception in thread “main” javax.naming.NoInitialContextException: Cannot instantiate class: weblogic.jndi.WLInitialContextFactory [Root exception is java.lang.ClassNotFoundException: weblogic.jndi.WLInitialContextFactory]
at javax.naming.spi.NamingManager.getInitialContext(Unknown Source)
at javax.naming.InitialContext.getDefaultInitCtx(Unknown Source)
at javax.naming.InitialContext.init(Unknown Source)
at javax.naming.InitialContext.(Unknown Source)
at com.cme.JMS.Sender.main(Sender.java:27)
Caused by: java.lang.ClassNotFoundException: weblogic.jndi.WLInitialContextFactory
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Unknown Source)
at com.sun.naming.internal.VersionHelper12.loadClass(Unknown Source)
at com.sun.naming.internal.VersionHelper12.loadClass(Unknown Source)
… 5 more
Can you help to check this?
Many Thanks,
Phung
Exception in thread “main” javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initial
at javax.naming.spi.NamingManager.getInitialContext(Unknown Source)
at javax.naming.InitialContext.getDefaultInitCtx(Unknown Source)
at javax.naming.InitialContext.getURLOrDefaultInitCtx(Unknown Source)
at javax.naming.InitialContext.lookup(Unknown Source)
at test.jms.Publisher.main(Publisher.java:20)