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>
The ConnectionFactory must implement XAConnectionFactory (Artemis does).
@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
receiveWith participates in the caller’s transaction.
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);
});
}
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.