ActiveMQ JMS 2.0 Implementation Guide: Simplified API, Transactions & Spring

meshIQ May 5, 2026

For most of JMS's lifetime, writing a simple producer required creating a ConnectionFactory, creating a Connection, starting it, creating a Session, creating a MessageProducer, creating a Message, calling send(), and then closing the producer, session, and connection with the close calls safely wrapped in finally blocks to prevent resource leaks. Every developer knew the pattern. Every developer wrote it slightly differently. Every code review had the same comments about resource management.

JMS 2.0 changed this. The simplified API introduces JMSContext, which combines Connection and Session into a single AutoCloseable object. A producer sends a message in two lines of code. Exceptions are runtime instead of checked. The ceremony is gone.

This guide covers the full ActiveMQ JMS 2.0 implementation: the simplified API and how it compares to JMS 1.1, feature support across Apache ActiveMQ® and Apache Artemis™, transaction support with failover safety, delivery-delay configuration, shared subscriptions, and Spring Boot integration patterns that avoid the most common performance pitfalls.

JMS 2.0 Support in ActiveMQ Apache ActiveMQ® and Apache Artemis™

Before writing any code, understand exactly which JMS 2.0 features are available on your broker and client version. Apache ActiveMQ®’s JMS 2.0 implementation is a work in progress, feature delivery happened across multiple 5.18.x and 6.x releases.

FeatureApache ActiveMQ® 5.18.xApache Artemis™ 2.x
JMS 2.0 API dependency
JMSContext
JMSProducer / JMSConsumer
JMSRuntimeException (unchecked)
receiveBody(Class)✅ (5.18.x+)
Delivery delay (setDeliveryDelay)✅ (broker requires schedulerSupport=true)
Async send with CompletionListener✅ (6.1.x+, 5.18.x backport)
Shared non-durable subscriptions🔄 (planned 6.2.x)
Shared durable subscriptions🔄 (planned 6.2.x)
Jakarta Messaging 3.1 namespace✅ (6.x native; 5.18.x via activemq-client-jakarta)

For Artemis, full JMS 2.0 support has been available since the 2.x line. For Apache ActiveMQ®, check the Apache JIRA tracking page (activemq.apache.org/jms2) against your specific version before using any feature listed as in-progress.

The critical implication: shared topic subscriptions are not available on Apache ActiveMQ® 5.18.x. If your application needs multiple consumers on the same durable topic subscription (the JMS 2.0 createSharedDurableConsumer pattern), you have two options on Apache ActiveMQ®: use Artemis, or use Apache ActiveMQ®’s Virtual Topics, which predate JMS 2.0 and provide the same semantics. 

We covered the Apache ActiveMQ® vs Apache Artemis™ architectural differences, including this feature gap, in our ActiveMQ Classic vs Artemis: 2026 Definitive Guide.

The JMS 2.0 Simplified API: JMSContext, JMSProducer, JMSConsumer

JMS 1.1 vs JMS 2.0: The Code Comparison

The scale of the simplification is best demonstrated by direct comparison. Both snippets below accomplish the same task: send a text message to a queue.

JMS 1.1 (Apache ActiveMQ® API):

JMS 1.1 (Apache ActiveMQ® API):
Connection connection = null;
try {
    connection = connectionFactory.createConnection(“user”, “password”);
    connection.start();
    Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
    MessageProducer producer = session.createProducer(queue);
    TextMessage message = session.createTextMessage(“Order processed: ” + orderId);
    message.setStringProperty(“orderId”, orderId);
    message.setIntProperty(“priority”, 5);
    producer.setPriority(5);
    producer.send(message);
} catch (JMSException e) {
    // checked exception — must be handled or declared
    logger.error(“Failed to send message”, e);
    throw new RuntimeException(e);
} finally {
    if (connection != null) {
        try { connection.close(); } catch (JMSException ignored) {}
    }
}
JMS 2.0 (Simplified API):
try (JMSContext context = connectionFactory.createContext(“user”, “password”)) {
    context.createProducer()
          .setProperty(“orderId”, orderId)
          .setPriority(5)
          .send(queue, “Order processed: ” + orderId);
}
// JMSRuntimeException is unchecked — catch only if your code needs to handle it

The JMS 2.0 version does not require a Message object, does not require explicit session management, does not require separate close() calls, and does not require a checked exception handler. The JMSContext implements AutoCloseable, so the try-with-resources block handles cleanup regardless of how the block exits.

JMSContext: Connection + Session in One Object

JMSContext is the entry point for all JMS 2.0 operations. Think of it as a Connection and Session merged into a single API. Create one from your ConnectionFactory:

// Java SE: default session mode is AUTO_ACKNOWLEDGE
JMSContext context = connectionFactory.createContext();

// With credentials
JMSContext context = connectionFactory.createContext(“username”, “password”);

// With explicit session mode
JMSContext context = connectionFactory.createContext(JMSContext.SESSION_TRANSACTED);

// Java EE / CDI injection (application server manages lifecycle)
@Inject
@JMSConnectionFactory(“jms/connectionFactory”)
private JMSContext context;

Session modes available on JMSContext:

ConstantValueBehavior
AUTO_ACKNOWLEDGE1Messages acknowledged automatically after onMessage() or synchronous receive() returns
CLIENT_ACKNOWLEDGE2Application calls message.acknowledge() explicitly; all previously unacknowledged messages on the session are acknowledged
DUPS_OK_ACKNOWLEDGE3Lazy acknowledgment may produce duplicates if the broker fails, and reduces overhead
SESSION_TRANSACTED0Explicit commit() / rollback() required; used for transactional message processing

JMSProducer: Method Chaining and Inline Properties

JMSProducer is a lightweight object, unlike JMS 1.1’s MessageProducer, you do not need to save it in a variable or close it. Create one on the fly from the context and chain your property settings directly into the send call:

// Send a string — no TextMessage creation needed
context.createProducer().send(queue, “Hello world”);

// Send with message properties (method chaining)
context.createProducer()
      .setProperty(“region”, “us-east”)
      .setProperty(“priority”, 8)
      .setDeliveryMode(DeliveryMode.PERSISTENT)
      .setTimeToLive(300_000L)   // 5 minutes TTL
      .send(topic, “Event fired”);

// Send a serializable object directly
context.createProducer().send(queue, new OrderEvent(orderId, amount));

// Send a byte array
context.createProducer().send(queue, new byte[]{0x01, 0x02, 0x03});

JMSProducer supports sending String, byte[], Map<String, Object>, Serializable, and Message objects directly, no type-specific message creation required for the primitive types.

JMSConsumer: Receiving Without Casts

// Synchronous receive with type — no cast
JMSConsumer consumer = context.createConsumer(queue);
String message = consumer.receiveBody(String.class, 5000L); // 5s timeout

// Returns null if timeout exceeded
if (message == null) {
    logger.warn(“No message received within timeout”);
}

// Asynchronous receive via MessageListener
consumer.setMessageListener(message -> {
    String body = message.getBody(String.class);  // JMS 2.0 type extraction
    processOrder(body);
});

// Durable subscriber (persists subscription across disconnects)
JMSConsumer durable = context.createDurableConsumer(topic, “my-subscription-name”);

Transactions with JMS 2.0: Commit, Rollback, and Failover Safety

Basic Transacted JMSContext

// Create a transacted context
try (JMSContext context = connectionFactory.createContext(JMSContext.SESSION_TRANSACTED)) {

    JMSProducer producer = context.createProducer();
    JMSConsumer consumer = context.createConsumer(inboundQueue);

    // Process a message atomically: consume from one queue, produce to another
    Message received = consumer.receive(5000L);
    if (received != null) {
        String orderId = received.getBody(String.class);
        // Perform business logic…
        producer.send(processedQueue, “Processed: ” + orderId);

        // Commit both the consume acknowledgment and the produce
        context.commit();
    }

} catch (JMSRuntimeException e) {
    // Session is automatically rolled back when context closes on exception
    logger.error(“Transaction failed, rolling back”, e);
    throw e;
}

Handling In-Doubt Transactions During HA Failover

This is the aspect of JMS transactions that most applications get wrong. When a commit() call is in-flight at the exact moment the broker fails over, the Failover Transport cannot determine whether the commit reached the broker or not. The result is an in-doubt transaction that the broker automatically rolls back to prevent ambiguity.

From the client’s perspective, an in-doubt transaction manifests as a JMSRuntimeException wrapping a TransactionRolledBackException. The in-flight commit is rolled back, not replayed. Your application must catch this and retry the entire transaction from the beginning.

private void processWithRetry(String orderId, int maxAttempts) {
    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
        try (JMSContext context =
                connectionFactory.createContext(JMSContext.SESSION_TRANSACTED)) {

            // All operations within the try block form one transaction
            context.createProducer().send(warehouseQueue, “Pack: ” + orderId);
            context.createProducer().send(shippingQueue, “Ship: ” + orderId);
            context.commit();
            return; // Success

        } catch (JMSRuntimeException e) {
            if (attempt == maxAttempts) {
                logger.error(“Transaction failed after {} attempts for order {}”,
                    maxAttempts, orderId);
                throw e;
            }
            logger.warn(“Transaction rolled back (attempt {}/{}), retrying: {}”,
                attempt, maxAttempts, e.getMessage());
            // Brief backoff before retry
            try { Thread.sleep(100L * attempt); } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(ie);
            }
        }
    }
}

Your retry logic must be idempotent, the same operation must produce the same result if executed multiple times. For order processing, this typically means using a unique order ID as a natural deduplication key and checking whether the order has already been processed before re-processing. 

We covered the Failover Transport’s in-doubt transaction behavior in depth in our High Availability Architecture Guide.

Delivery Delay: Scheduling Messages in the Future

JMS 2.0 introduces setDeliveryDelay() on the producer, which instructs the broker to hold the message and deliver it only after the specified delay. On  Apache ActiveMQ®, this feature relies on the broker’s scheduler, it is not a client-side hold.

Required Broker Configuration

<!– activemq.xml — enable scheduler support (REQUIRED for delivery delay) –>
<broker xmlns=”http://activemq.apache.org/schema/core”
        brokerName=”prod-broker”
        schedulerSupport=”true”
        dataDirectory=”/var/activemq/data”>
  <!– dataDirectory must be set — scheduler data is stored on the shared path –>
  <!– For HA setups, this must point to the shared filesystem –>
  <!– Omitting this is the most common delivery delay misconfiguration –>

Without schedulerSupport=”true”, the AMQ_SCHEDULED_DELAY property is silently ignored, and messages are delivered immediately. There are no error messages; it just arrives without the expected delay. This is consistently the most reported delivery delay bug report on the mailing list, and consistently has the same root cause: missing scheduler configuration.

Also note: the HA scheduler directory interaction we covered in the HA guide applies here, too. Scheduled messages are stored in the scheduler’s own data structure, if this is not on the shared filesystem in an Apache ActiveMQ® HA deployment, scheduled messages are lost on failover.

JMS 2.0 Delivery Delay API

// JMS 2.0: set delivery delay on the producer
try (JMSContext context = connectionFactory.createContext()) {
    context.createProducer()
          .setDeliveryDelay(30_000L)   // 30 seconds in milliseconds
          .send(retryQueue, “Retry this operation”);
}

ActiveMQ  Apache ActiveMQ®: AMQ_SCHEDULED_DELAY (Alternative for Pre-5.18.x)

For  Apache ActiveMQ® versions before JMS 2.0 support, the proprietary scheduler API uses message properties:

// Classic scheduler via message properties (JMS 1.1 compatible)
try (JMSContext context = connectionFactory.createContext()) {
    // Create message explicitly to set ActiveMQ-specific property
    TextMessage msg = context.createTextMessage(“Retry order: ” + orderId);
    // Delay: 30 seconds
    msg.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 30_000L);
    // Optional: repeat delivery N times
    msg.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, 3);
    // Optional: period between repeats
    msg.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, 10_000L);
    context.createProducer().send(retryQueue, msg);
}

The AMQ_SCHEDULED_DELAY approach works on  Apache ActiveMQ® 5.x regardless of JMS 2.0 support level and gives finer control (repeat counts, periods). It is the correct choice for scheduled delivery on  Apache ActiveMQ® when the Apache Artemis™ migration is not planned.

Async Sends with CompletionListener

JMS 2.0 introduced asynchronous send with a CompletionListener, the producer fires the send and receives a callback when the broker has acknowledged it, rather than blocking.

// Async send with CompletionListener (requires Classic 6.1.x+ or Artemis)
try (JMSContext context = connectionFactory.createContext()) {
    context.createProducer()
          .setAsync(new CompletionListener() {
              @Override
              public void onCompletion(Message message) {
                  logger.info(“Message {} delivered successfully”,
                      message.getJMSMessageID());
              }
              @Override
              public void onException(Message message, Exception exception) {
                  logger.error(“Message {} failed to deliver: {}”,
                      message.getJMSMessageID(), exception.getMessage());
                  // Handle failure: retry, DLQ routing, alert
              }
          })
          .send(queue, “Event payload”);
    // Returns immediately — broker acknowledgment arrives via callback
}

Important constraint: the JMS 2.0 spec requires that the Message object is not accessed by the application after the send() call returns and before the CompletionListener is invoked. Do not re-use or modify the message object between the send and the callback.

The CompletionListener approach is the JMS 2.0 equivalent of useAsyncSend=true on the Apache ActiveMQ® connection factory (which we covered in ActiveMQ Performance Tuning: 10x Throughput), but with explicit per-message acknowledgment callbacks rather than fire-and-forget.

Shared Topic Subscriptions: Scaling Topic Consumers

JMS 1.1 durable topic subscriptions have a fundamental limitation: only one consumer can be active at a time per subscription. If you want multiple threads or processes to consume from the same durable topic subscription for load balancing, JMS 1.1 has no built-in support.

JMS 2.0 introduces shared subscriptions, multiple consumers can share a single topic subscription, with the broker distributing messages among them:

// Shared durable consumer (available on Artemis; Classic 6.2.x planned)
// Multiple JMSConsumer instances on the same subscription share the work
try (JMSContext context = connectionFactory.createContext()) {

    // Instance 1 (on one thread or service instance)
    JMSConsumer consumer1 = context.createSharedDurableConsumer(
        topic, “my-shared-subscription”);

    // Instance 2 (on a different thread or service instance — same sub name)
    JMSConsumer consumer2 = context.createSharedDurableConsumer(
        topic, “my-shared-subscription”);

    // Messages are distributed between consumer1 and consumer2
    // rather than both receiving every message
}

Apache ActiveMQ® Virtual Topics as the equivalent: Since shared subscriptions are not available on  5.18.x, the established pattern is Virtual Topics, an Apache ActiveMQ®-specific feature that predates JMS 2.0 and achieves the same consumer parallelism. Instead of a single topic subscription, each consumer team subscribes to a queue named Consumer.team1.VirtualTopic.MyTopic, which receives a copy of every message published to VirtualTopic.MyTopic. Multiple instances within the same team compete for messages on that queue, providing load balancing.

We covered Virtual Topics in the context of MQTT scalability in our MQTT Protocol Setup Guide. The same pattern applies to JMS consumers.

Spring Boot Integration: JmsTemplate and @JmsListener

The PooledConnectionFactory Requirement

This is the single most performance-damaging misconfiguration in Spring Boot JMS applications with ActiveMQ:

// WRONG: JmsTemplate without pooling
// Creates a new connection + session + producer on EVERY send
// Then immediately closes them all
@Autowired JmsTemplate jmsTemplate;  // backed by plain ActiveMQConnectionFactory

Every jmsTemplate.send() or jmsTemplate.convertAndSend() call opens a new connection to the broker, creates a session and producer, sends the message, and closes all three. For an application sending 100 messages per second, that is 100 connection open/close cycles per second, each involving a TCP handshake, authentication, and protocol negotiation with the broker.

// CORRECT: JmsTemplate with PooledConnectionFactory
@Bean
public ConnectionFactory connectionFactory() {
    ActiveMQConnectionFactory activeMQCF = new ActiveMQConnectionFactory();
    activeMQCF.setBrokerURL(“failover:(ssl://broker1:61617,ssl://broker2:61617)”
        + “?randomize=false”);
    activeMQCF.setUserName(“service-account”);
    activeMQCF.setPassword(“service-password”);

    // Pool connections — reuse across JmsTemplate calls
    JmsPoolConnectionFactory pool = new JmsPoolConnectionFactory();
    pool.setConnectionFactory(activeMQCF);
    pool.setMaxConnections(10);
    pool.setMaxSessionsPerConnection(100);
    pool.setBlockIfSessionPoolIsFull(true);
    pool.setBlockIfSessionPoolIsFullTimeout(5000L);
    return pool;
}

@Bean
public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
    JmsTemplate template = new JmsTemplate(connectionFactory);
    template.setDefaultDestinationName(“orders.queue”);
    template.setDeliveryMode(DeliveryMode.PERSISTENT);
    template.setSessionTransacted(true); // Enable JMS local transactions
    return template;
}

The JmsPoolConnectionFactory from org.messaginghub:pooled-jms (formerly activemq-jms-pool) is the correct pooling library. The older activemq-pool (PooledConnectionFactory) has known JMS 2.0 compatibility issues specifically, it throws AbstractMethodError when Spring’s JmsTemplate calls setDeliveryDelay() because the pool does not implement that JMS 2.0 method. Use the newer JmsPoolConnectionFactory for JMS 2.0 applications.

Spring Boot @JmsListener Pattern

@Component
public class OrderConsumer {

    @JmsListener(destination = “orders.queue”,
                containerFactory = “jmsListenerContainerFactory”)
    public void onOrder(String orderPayload) {
        // Spring auto-acknowledges after method returns without exception
        // For exception: Spring rolls back and redelivers per redelivery policy
        orderService.processOrder(orderPayload);
    }

    // Typed message reception with access to headers
    @JmsListener(destination = “payments.queue”)
    public void onPayment(Message<PaymentEvent> message) {
        PaymentEvent event = message.getPayload();
        String correlationId = message.getHeaders()
            .get(“JMSCorrelationID”, String.class);
        paymentService.process(event, correlationId);
    }
}

// JmsListenerContainerFactory for topic subscriptions
@Bean
public DefaultJmsListenerContainerFactory topicListenerFactory(
        ConnectionFactory connectionFactory) {
    DefaultJmsListenerContainerFactory factory =
        new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setPubSubDomain(true);          // Topic mode
    factory.setSubscriptionDurable(true);   // Survive restarts
    factory.setClientId(“my-service-” +
        InetAddress.getLocalHost().getHostName()); // Unique per instance
    factory.setSessionTransacted(true);
    factory.setConcurrency(“1-5”);          // Min 1, max 5 concurrent consumers
    return factory;
}

JMS 1.1 to 2.0 Migration: What Changes and What Stays

The JMS 1.1 API is fully preserved in JMS 2.0. Existing code using Connection, Session, MessageProducer, and MessageConsumer continues to work without modification. Migration is incremental: introduce JMS 2.0 patterns in new code while leaving existing code as-is.

What to adopt first (highest impact, lowest risk):

  1. JMSContext with try-with-resources: eliminates resource leak bugs in new code
  2. JMSProducer method chaining: removes boilerplate MessageProperty setting
  3. receiveBody(Class): removes casting in consumer code

What to handle carefully:

  • Exception handling, JMSRuntimeException is unchecked; verify that existing catch-all exception handlers still catch it 
  • Delivery delay requires broker configuration change (schedulerSupport=true
  • Shared subscriptions – Apache Artemis™ only (Apache ActiveMQ® 5.18.x not supported)

Maven dependency for JMS 2.0 with Apache ActiveMQ®:

<!– pom.xml — JMS 2.0 client for ActiveMQ Classic –>
<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-client</artifactId>
    <version>5.18.3</version>  <!– or latest supported version –>
</dependency>

<!– For Jakarta Messaging 3.1 namespace (Jakarta EE 9+) –>
<!– Use activemq-client-jakarta in Classic < 6.0 –>
<!– In Classic 6.0+ this module is no longer needed –>
<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-client-jakarta</artifactId>
    <version>5.18.3</version>
</dependency>

<!– Pooling for Spring applications (JMS 2.0 compatible) –>
<dependency>
    <groupId>org.messaginghub</groupId>
    <artifactId>pooled-jms</artifactId>
    <version>3.1.0</version>
</dependency>

The transition from javax.jms to jakarta.jms namespace is handled by the activemq-client-jakarta module in  5.18.x, Apache ActiveMQ® existing Spring bean definitions do not change, only the import statements in your application code need updating when making the namespace switch. In Apache ActiveMQ® 6.0+, this module is no longer needed as the main client artifact uses Jakarta natively.

JMS 2.0 Is Ready for Production, With the Right Configuration

JMS 2.0 delivers on its promise of simpler, less error-prone messaging code. The JMSContext + try-with-resources pattern eliminates the resource leak bugs that plagued JMS 1.1 codebases. The method chaining API eliminates the multi-step message property boilerplate. Unchecked exceptions remove the omnipresent throws JMSException from method signatures across entire application tiers.

The practical production prerequisites, pooled connection factories, broker scheduler configuration for delivery delay, idempotent retry for transactional failover, and careful version-checking against Apache ActiveMQ®’s rolling JMS 2.0 implementation are all addressable once you know they exist. This guide gives you the configuration foundation. meshIQ provides the operational expertise.

Get expert guidance on your ActiveMQ JMS 2.0 implementation → Talk to an Expert

Frequently Asked Questions

Q1. Does ActiveMQ support JMS 2.0? 

Apache ActiveMQ® 5.18.x supports core JMS 2.0 features including JMSContext, JMSProducer, JMSConsumer, delivery delay, and async sends (added in 6.1.x). Shared topic subscriptions are planned for a later release. Apache Artemis™ provides full JMS 2.0 support. Verify your specific feature requirements against the Apache ActiveMQ® version’s JMS 2.0 tracking page before committing to an implementation.

Q2. What is JMSContext in JMS 2.0? 

JMSContext is the central object of the JMS 2.0 simplified API. It combines JMS 1.1 Connection + Session into a single AutoCloseable object, enabling try-with-resources usage and eliminating the connection management boilerplate that defined JMS 1.1 applications.

Q3. What is the difference between JMS 1.1 and JMS 2.0? 

JMS 2.0 adds the simplified API (JMSContext/JMSProducer/JMSConsumer), unchecked exceptions (JMSRuntimeException), delivery delay, async sends, and shared subscriptions. The JMS 1.1 API remains fully supported, migration to the simplified API is opt-in and incremental.

Q4. How do I implement JMS transactions with JMS 2.0? 

Use connectionFactory.createContext(JMSContext.SESSION_TRANSACTED). Call context.commit() to commit and context.rollback() to roll back. Handle JMSRuntimeException from commit(), during HA failover, in-doubt transactions are rolled back. Implement idempotent retry logic to safely re-attempt failed transactions.

Q5. How do I use JMS 2.0 with Spring Boot and ActiveMQ? 

Add spring-boot-starter-activemq. Configure spring.activemq.broker-url in application.properties. Use @JmsListener for consumers. Critically: always wrap ActiveMQConnectionFactory in JmsPoolConnectionFactory (from org.messaginghub:pooled-jms), raw JmsTemplate without pooling creates a new connection on every send.

Cookies preferences

Others

Other uncategorized cookies are those that are being analyzed and have not been classified into a category as yet.

Necessary

Necessary
Necessary cookies are absolutely essential for the website to function properly. These cookies ensure basic functionalities and security features of the website, anonymously.

Advertisement

Advertisement cookies are used to provide visitors with relevant ads and marketing campaigns. These cookies track visitors across websites and collect information to provide customized ads.

Analytics

Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics the number of visitors, bounce rate, traffic source, etc.

Functional

Functional cookies help to perform certain functionalities like sharing the content of the website on social media platforms, collect feedbacks, and other third-party features.

Performance

Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.