XA Distributed Transactions
XA transactions coordinate JMS message consumption with other XA resources (e.g., database) in a single atomic transaction.
The @JmsListener, Reactive Messaging connector, and JmsClient all support XA.
Prerequisites
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-narayana-jta</artifactId>
</dependency>
You also need an XA-capable connection factory.
For each connection factory identifier, Quarkus JMS first looks for an XAConnectionFactory bean with the same qualifier.
If there is none, it checks whether the ConnectionFactory bean itself implements XAConnectionFactory.
Among the tested providers, Apache ActiveMQ Artemis (with quarkus.artemis.xa-enabled=true), Apache ActiveMQ Classic and IBM MQ support XA.
For ActiveMQ Classic and IBM MQ, produce an XAConnectionFactory bean next to the ConnectionFactory bean.
See JMS Providers for examples.
XA is not available in JMS 1.1 compatibility mode (quarkus.jms.jms11=true).
Database resources must also be XA-capable, for example with quarkus.datasource.jdbc.transactions=xa.
@JmsListener with XA
@JmsListener(value = "orders", acknowledge = JmsListener.AcknowledgeMode.XA)
public void process(Order order) {
entityManager.persist(order);
// Both JMS consume and DB insert commit atomically
}
The container manages the full XA lifecycle on the listener thread: tm.begin() → enlist XA resource → receive → invoke → tm.commit() (or rollback on error).
Reactive Messaging with XA
mp.messaging.incoming.orders.connector=smallrye-jms
mp.messaging.incoming.orders.transaction-mode=xa
@Incoming("orders")
public void process(Order order) {
entityManager.persist(order);
}
XA with the Reactive Messaging connector uses a suspend/resume mechanism:
-
The poller starts an XA transaction, receives the message, and suspends the transaction
-
A custom invoker resumes the transaction on the processing thread before the
@Incomingmethod -
After processing, the transaction is suspended again
-
The commit handler resumes and commits (or rollback handler resumes and rolls back)
This works with @Blocking, @NonBlocking, and @RunOnVirtualThread — the transaction follows the message via metadata, not thread-local state.
JmsClient with XA
Operations on JmsQueue, JmsTopic and JmsClient participate in the caller’s transaction.
When one of them is called inside an active transaction, for example in a @Transactional method, the extension creates an XA context, enlists it in the transaction, and closes it when the transaction completes.
This applies to sends, receives, receiveWith and execute.
A message sent inside a transaction is only delivered when the transaction commits:
@Transactional
void placeOrder(Order order) {
entityManager.persist(order);
orders.send(order);
// the insert and the send commit together, or are both rolled back
}
When called inside a @Transactional method, all receives and database operations commit or roll back atomically:
@Transactional
void processBatch() {
orders.receiveWith(receiver -> {
receiver.receive().ifPresent(entityManager::persist);
receiver.receive().ifPresent(entityManager::persist);
});
}
Without an XA connection factory
If a transaction is active but no XA-capable connection factory is available, the operation uses a local transacted JMS session instead. The session is committed after the transaction commits, and rolled back if the transaction rolls back.
This is not atomic: if the JMS commit fails after the database commit, the database changes are kept but the JMS operation is lost. Use an XA connection factory when both must succeed or fail together.
XA with Virtual Threads
@JmsListener(value = "orders", acknowledge = JmsListener.AcknowledgeMode.XA,
concurrency = 1, maxConcurrency = 10)
@RunOnVirtualThread
public void process(Order order) {
entityManager.persist(order);
}
Multiple messages can be processed concurrently on virtual threads, each with its own independent XA transaction.
The same applies to Reactive Messaging:
@Incoming("orders")
@RunOnVirtualThread
public void process(Order order) {
entityManager.persist(order);
}
Chained XA Listeners
Multiple XA listeners can form a processing pipeline where each stage atomically consumes, writes to the database, and forwards to the next stage:
@ApplicationScoped
public class OrderPipeline {
@Inject JmsClient jms;
@Inject EntityManager em;
@JmsListener(value = "incoming-orders", acknowledge = JmsListener.AcknowledgeMode.XA)
public void validate(Order order) {
em.persist(new AuditLog("validated", order.id()));
jms.queue("validated-orders").send(order);
// XA commit: consume + DB insert + JMS send are atomic
// If this method throws, all three are rolled back
}
@JmsListener(value = "validated-orders", acknowledge = JmsListener.AcknowledgeMode.XA)
public void fulfill(Order order) {
em.persist(new AuditLog("fulfilled", order.id()));
// XA commit: consume + DB insert are atomic
}
}
If the first listener fails, the message stays on incoming-orders and nothing is sent to validated-orders.
If the second listener fails, the message stays on validated-orders and the DB insert is rolled back.