The Java Message Service (JMS) API is the standard Java EE / Jakarta EE API for sending, receiving, and reading messages in an asynchronous, loosely coupled way. Rather than having Service A call Service B directly, JMS lets A drop a message onto a queue or topic and carry on. Service B picks it up whenever it is ready. In this guide you will learn the key JMS concepts, study both queue (point-to-point) and topic (publish-subscribe) messaging models, and walk through a fully working Java example using Apache ActiveMQ — the most widely deployed open-source JMS broker.
Why Asynchronous Messaging?
Synchronous REST calls work well for simple request-reply, but they create tight coupling: if Service B is slow or down, Service A must wait or fail. JMS solves this with a broker as an intermediary — producers and consumers are decoupled in time, space, and failure domains. This makes it a foundational pattern for microservices, event-driven architectures, and any workflow where tasks should be processed in the background.
Core JMS Concepts
| Concept | Description |
|---|---|
| Broker | The message server that routes and stores messages (e.g. ActiveMQ, RabbitMQ, IBM MQ). |
| Destination | Either a Queue (point-to-point) or a Topic (publish-subscribe). |
| ConnectionFactory | Creates JMS connections to the broker. |
| Connection | An active connection to the broker. |
| Session | A single-threaded context for producing and consuming messages. |
| Producer | Sends messages to a destination. |
| Consumer | Receives messages from a destination (synchronously or via a listener). |
| Message | The unit of data transferred (TextMessage, ObjectMessage, BytesMessage, MapMessage). |
Point-to-Point vs Publish-Subscribe
| Feature | Queue (P2P) | Topic (Pub-Sub) |
|---|---|---|
| Destination type | Queue | Topic |
| How many consumers receive each message? | Exactly one | All active subscribers |
| Messages held for offline consumers? | Yes (until consumed) | Only with durable subscriptions |
| Use case | Task queues, order processing | Notifications, event broadcasting |
Setup: Add ActiveMQ to Maven
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.18.3</version>
</dependency>
Example 1: Sending and Receiving from a Queue
import org.apache.activemq.ActiveMQConnectionFactory;
import jakarta.jms.*;
public class QueueDemo {
private static final String BROKER_URL = "tcp://localhost:61616";
private static final String QUEUE_NAME = "order.queue";
// --- PRODUCER ---
public static void sendMessage(String text) throws JMSException {
ConnectionFactory factory = new ActiveMQConnectionFactory(BROKER_URL);
try (Connection conn = factory.createConnection()) {
conn.start();
// AUTO_ACKNOWLEDGE: broker removes message once consumer receives it
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageProducer producer = session.createProducer(queue);
TextMessage message = session.createTextMessage(text);
producer.send(message);
System.out.println("Sent: " + text);
}
}
// --- CONSUMER (synchronous) ---
public static void receiveMessage() throws JMSException {
ConnectionFactory factory = new ActiveMQConnectionFactory(BROKER_URL);
try (Connection conn = factory.createConnection()) {
conn.start();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer consumer = session.createConsumer(queue);
// Block for up to 5 seconds waiting for a message
Message msg = consumer.receive(5000);
if (msg instanceof TextMessage textMsg) {
System.out.println("Received: " + textMsg.getText());
} else {
System.out.println("No message received within timeout.");
}
}
}
public static void main(String[] args) throws JMSException {
sendMessage("Order #1001: 2x Laptop, 3x Mouse");
receiveMessage();
}
}
Example 2: Asynchronous MessageListener
Instead of blocking on receive(), register a MessageListener callback that fires whenever a message arrives:
import jakarta.jms.*;
import org.apache.activemq.ActiveMQConnectionFactory;
public class AsyncConsumer implements MessageListener {
@Override
public void onMessage(Message message) {
try {
if (message instanceof TextMessage tm) {
System.out.println("[ASYNC] Received: " + tm.getText());
}
} catch (JMSException e) {
System.err.println("Error processing message: " + e.getMessage());
}
}
public static void main(String[] args) throws Exception {
ConnectionFactory factory =
new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection conn = factory.createConnection();
conn.start();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("order.queue");
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(new AsyncConsumer()); // register listener
System.out.println("Waiting for messages. Press CTRL+C to exit.");
Thread.sleep(Long.MAX_VALUE); // keep alive
}
}
Example 3: Publish-Subscribe with a Topic
import jakarta.jms.*;
import org.apache.activemq.ActiveMQConnectionFactory;
public class TopicDemo {
static final String BROKER = "tcp://localhost:61616";
static final String TOPIC = "news.tech";
static void publish(String text) throws JMSException {
ConnectionFactory cf = new ActiveMQConnectionFactory(BROKER);
try (Connection conn = cf.createConnection()) {
conn.start();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC);
session.createProducer(topic).send(session.createTextMessage(text));
System.out.println("Published: " + text);
}
}
static void subscribe(String subscriberName) throws JMSException {
ConnectionFactory cf = new ActiveMQConnectionFactory(BROKER);
try (Connection conn = cf.createConnection()) {
conn.start();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC);
MessageConsumer sub = session.createConsumer(topic);
Message msg = sub.receive(3000);
if (msg instanceof TextMessage tm)
System.out.println(subscriberName + " got: " + tm.getText());
}
}
public static void main(String[] args) throws Exception {
// Start subscribers BEFORE publishing so they are active
Thread t1 = new Thread(() -> { try { subscribe("SubA"); } catch (JMSException e) { e.printStackTrace(); } });
Thread t2 = new Thread(() -> { try { subscribe("SubB"); } catch (JMSException e) { e.printStackTrace(); } });
t1.start(); t2.start();
Thread.sleep(200); // allow subscribers to connect
publish("Java 24 Released!");
t1.join(); t2.join();
}
}
Sample Output
-- Queue Demo --
Sent: Order #1001: 2x Laptop, 3x Mouse
Received: Order #1001: 2x Laptop, 3x Mouse
-- Topic Demo --
Published: Java 24 Released!
SubA got: Java 24 Released!
SubB got: Java 24 Released!
JMS with Spring Boot (JmsTemplate)
In a Spring Boot application the broker configuration and session management are abstracted away by JmsTemplate and @JmsListener:
// application.properties
// spring.activemq.broker-url=tcp://localhost:61616
@Component
public class OrderService {
@Autowired
private JmsTemplate jmsTemplate;
public void placeOrder(String orderDetails) {
jmsTemplate.convertAndSend("order.queue", orderDetails);
}
}
@Component
public class OrderProcessor {
@JmsListener(destination = "order.queue")
public void processOrder(String orderDetails) {
System.out.println("Processing: " + orderDetails);
}
}
Best Practices
- Use connection pooling — creating a new
Connectionper message is expensive. UseCachingConnectionFactoryin Spring or a pool in plain Java. - Prefer transactional sessions for critical messages — use
SESSION_TRANSACTEDand callsession.commit()so messages are only removed after successful processing. - Configure dead-letter queues (DLQ) — messages that fail repeatedly should be routed to a DLQ for manual inspection rather than silently discarded.
- Set message expiry (TTL) — use
producer.setTimeToLive(milliseconds)to prevent stale messages from being processed. - Monitor queue depth — a growing queue signals that consumers cannot keep up; alert before it becomes a problem.
See Also
- Building a REST API with Spring Boot
- Java Comparable vs Comparator: The Definitive Guide
- Java Streams API: The Complete Reference Guide
Conclusion
JMS provides a battle-tested, vendor-neutral API for asynchronous messaging in Java. Use queues when exactly one consumer should handle each message (task distribution, order processing), and topics when every subscriber needs a copy (notifications, audit events). For Spring Boot applications, JmsTemplate and @JmsListener abstract away the boilerplate, leaving you to focus on business logic. Combine transactional sessions, dead-letter queues, and TTL settings to build a messaging layer that is reliable under real-world conditions.