Messaging 101 with JMS: Async without Fear. I have been bouncing between web apps stuffed with AJAX and talks about services all week, and a theme keeps coming up in teams I visit: async is scary. People picture threads, stuck sockets, and mystery bugs at 2 AM. It does not have to be like that. With the Java Message Service, you can ship work off to queues, smooth out spikes, and keep user flows snappy. The trick is to think in messages and to keep your contracts clean. Tonight I want to boil down what is working for us with JMS right now with lessons that will still make sense when the next buzzword arrives.
Context
JMS is everywhere in the shop floor of Java servers. WebLogic ships with a solid provider. JBoss has queues out of the box. ActiveMQ from Apache is moving fast and is friendly for dev boxes. Plenty of teams still run WebSphere MQ in the back office. With Java 5 bringing nicer concurrency and annotations on the table, you might be wondering when to send a message instead of calling a method or an HTTP endpoint. If your app is serving logged in users and you need things to feel instant, asynchronous messaging is your friend. You confirm quickly, push the heavy work to workers, and let the broker buffer the rush when traffic spikes.
Definitions
Before we go to code, some quick terms that matter when you build real systems:
- JMS: the Java API for messaging. It hides the provider details so your code can move between ActiveMQ, WebLogic JMS, WebSphere MQ, and friends.
- Queue: point to point. One consumer gets each message. Use this for work items like email send, thumbnail build, payment post.
- Topic: publish and subscribe. Many consumers can receive the same message. Use this for events like user signed up, order shipped, cache invalidated.
- Persistent vs non persistent: persistent messages are stored by the broker so you do not lose them on restart. Non persistent are faster but volatile. For money or user data pick persistent.
- Ack modes: AUTO is easy, CLIENT gives you control, DUPS OK is faster but you might see repeats. If you must be safe, start with AUTO or CLIENT plus idempotent logic.
- Transactions: you can wrap send and receive in a transaction. This is useful to avoid losing messages on crash. Keep the scope small. XA across DB and JMS works but is heavy.
- Message selectors: SQL like filters on headers and properties. Handy to route without new queues.
- JMSCorrelationID and JMSReplyTo: keys for request and reply flows. Set ReplyTo so the worker knows where to answer. Use CorrelationID to match reply to request.
- Dead letter queue: where poison messages go after retries. Always define one and monitor it.
Examples
Let us wire a basic producer and consumer first. This is plain JMS without any big framework.
import javax.jms.*;
import javax.naming.InitialContext;
public class InvoiceProducer {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext(); // from jndi.properties
ConnectionFactory cf = (ConnectionFactory) ctx.lookup("jms/ConnectionFactory");
Queue queue = (Queue) ctx.lookup("jms/InvoiceQueue");
Connection conn = cf.createConnection();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(queue);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
TextMessage msg = session.createTextMessage("{\"invoiceId\":1234}");
msg.setStringProperty("tenant", "acme");
msg.setJMSCorrelationID("req-1234");
producer.send(msg);
producer.close();
session.close();
conn.close();
}
}Now a consumer that does the work. Notice the simple error handling and the quick exit on failure. You need a retry plan and a dead letter queue behind this.
public class InvoiceConsumer implements MessageListener {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ConnectionFactory cf = (ConnectionFactory) ctx.lookup("jms/ConnectionFactory");
Queue queue = (Queue) ctx.lookup("jms/InvoiceQueue");
Connection conn = cf.createConnection();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(queue, "tenant = 'acme'");
consumer.setMessageListener(new InvoiceConsumer());
conn.start(); // start delivery
System.out.println("Listening. Ctrl C to quit.");
}
@Override
public void onMessage(Message message) {
try {
TextMessage tm = (TextMessage) message;
String json = tm.getText();
// do work
System.out.println("Processing " + json);
// if something fails, throw a RuntimeException to trigger redelivery
} catch (Exception e) {
throw new RuntimeException("work failed", e);
}
}
}If you are using Spring, the JmsTemplate and the message listener container remove a lot of the ceremony.
// applicationContext.xml (snippet)
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://localhost:61616"/>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="pubSubDomain" value="false"/>
</bean>
<bean id="listenerContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destinationName" value="jms/InvoiceQueue"/>
<property name="messageListener" ref="invoiceHandler"/>
<property name="sessionTransacted" value="true"/>
<property name="concurrency" value="5"/>
</bean>// Producer with Spring
jmsTemplate.convertAndSend("jms/InvoiceQueue", payload, m -> {
m.setStringProperty("tenant", "acme");
m.setJMSCorrelationID("req-1234");
return m;
});// Listener bean
public class InvoiceHandler implements MessageListener {
public void onMessage(Message message) {
// Spring wrapped transaction if configured
// do work, throw unchecked to trigger rollback and redelivery
}
}Need request and reply without blocking the web thread? Send with JMSReplyTo, handle replies on a topic, and stash pending requests in a map keyed by JMSCorrelationID. A small sample:
// request
Destination replyTo = session.createTemporaryQueue();
TextMessage msg = session.createTextMessage("{\"cmd\":\"price\"}");
msg.setJMSReplyTo(replyTo);
msg.setJMSCorrelationID("abc-42");
producer.send(msg);
// reply listener
MessageConsumer replies = session.createConsumer(replyTo, "JMSCorrelationID = 'abc-42'");
replies.setMessageListener(m -> {
// resolve promise, notify UI via comet or page refresh
});On the server side you can also use message driven beans if you are on an app server. They keep the plumbing tidy and let the container retry on failure.
import javax.ejb.MessageDriven;
import javax.jms.*;
@MessageDriven(mappedName = "jms/InvoiceQueue")
public class InvoiceBean implements MessageListener {
public void onMessage(Message message) {
// do work
}
}The message driven bean route is great when your team already runs an app server and you want simple deployment and pooling. The Spring route shines when you want the same model in a plain Tomcat world.
Counterexamples
There are times when a message queue is the wrong tool.
- Read heavy low latency requests: fetching product details or user profile fits HTTP or direct calls. A queue just adds delay.
- Large payloads: video files or big binary blobs do not sit well in a broker. Store in a file server or S3 like storage and pass a pointer in the message.
- Cross service transactions with two phase commit: it can be done, but the cost in setup and failure cases is steep. Many teams keep DB and JMS work in separate small steps and reach eventual consistency with retries.
- Strict ordering across many keys: a single queue preserves order only per consumer. If your business needs global order you will hit throughput limits fast.
- When ops is not ready: no monitoring, no disk plan, no backup, no dead letter queues. That is a recipe for a bad night.
Decision rubric
When a team asks me if JMS fits, we go through these checks. If most boxes say yes, I lean to messages.
- Does the user need instant feedback while heavy work continues later? Yes points to queue.
- Can the work be expressed as an independent command or event? Yes points to queue or topic.
- Is repeat delivery acceptable if your handler is idempotent? Yes points to simpler ack modes.
- Do you need to buffer bursts? Yes favors a broker with persistent messages.
- Do you have a clear retry plan and a dead letter queue? Yes is a must before go live.
- Is the payload small and self contained? Yes is good for a message. If not, pass a reference.
- Do you need fan out to many services? Yes points to topics with durable subscribers.
- Do you really need XA? If you are unsure, start without XA. Use a transactional session for the consumer and keep DB writes and send in separate steps with retries.
- Is monitoring in place? Queues, consumers, pending counts, dead letters, average age. If you cannot see it, you cannot fix it.
One more tip. If you lose sleep over a message being processed twice, add a business id to the payload and keep a table of processed ids. Then your consumer becomes safe to repeat. That pattern unlocks AUTO ack and less stress.
Lesson learned
Async is not scary if you keep the shape of your messages small and your contracts clear. Queues for work, topics for events, persistent for safety, idempotent for peace. Wrap each consumer with a retry policy and a dead letter queue. Keep XA rare. Watch your queues just like you watch CPU and memory. With that base, JMS turns into a pressure valve for your app. The web tier stays quick, the workers chew through the backlog, and the business sleeps better.
Right now the tools are good enough and the API is stable. ActiveMQ is friendly for local runs. The big vendors ship mature brokers. Spring keeps the code light. You do not need a big rewrite to get the benefits. Start with one slow step in your app, push it to a queue, and measure. Make it boring and you will get async without fear.