skip to content

How would you configure a Spring Boot application to run one transaction spanning a database and a JMS broker with Atomikos or Narayana?

level: seniorimportance: should knowfreq 35%

answer

  1. provider starter → JtaTransactionManager auto-config
  2. XADataSource + XAConnectionFactory, both pooled
  3. transacted non-auto-ack JMS sessions
  4. durable tx-log dir + unique node id for recovery
  5. plain @Transactional now spans both

basics

~20 s

Add a JTA provider starter (Atomikos or Narayana). Configure the DataSource as an XADataSource and the JMS ConnectionFactory as XA. Spring Boot auto-creates a JtaTransactionManager and enlists both, so a plain @Transactional method commits DB and JMS together via 2PC.

solid answer

~40 s

Put a JTA provider on the classpath — `spring-boot-starter-jta-atomikos` or the Narayana starter. Then both resources must be XA-capable: expose the database via a driver `XADataSource` (e.g. `PGXADataSource`) wrapped by the provider's pooling XA datasource, and the JMS `ConnectionFactory` must be an `XAConnectionFactory` wrapped for pooling. Spring Boot's JTA auto-config detects the provider, builds a `JtaTransactionManager` (delegating to the provider's `UserTransaction`/`TransactionManager`), and enlists both wrapped resources. A normal `@Transactional` service method then participates in one global transaction: `JdbcTemplate.update` plus `JmsTemplate.convertAndSend` commit or roll back atomically via 2PC. You must also give the coordinator a **durable transaction-log directory** and, in clustered/containerised deployments, a **unique node id** so XA recovery works. Enable transacted JMS sessions so the send enlists rather than auto-acking.

code

java · 49 lines
java
// application.yml (Atomikos)
// spring:
//   datasource:
//     xa:
//       data-source-class-name: org.postgresql.xa.PGXADataSource
//       properties:
//         url: jdbc:postgresql://db:5432/app
//   jta:
//     enabled: true
//     transaction-manager-id: order-svc-node-1   # UNIQUE per instance
//     atomikos:
//       properties:
//         log-base-dir: /var/lib/atomikos/log     # DURABLE volume

@Configuration
class JmsXaConfig {

    // Wrap an XA JMS factory so sends enlist in the global tx.
    @Bean
    ConnectionFactory jmsConnectionFactory() {
        ActiveMQXAConnectionFactory xa =
            new ActiveMQXAConnectionFactory("tcp://broker:61616");
        AtomikosConnectionFactoryBean pooled = new AtomikosConnectionFactoryBean();
        pooled.setUniqueResourceName("orders-jms");
        pooled.setXaConnectionFactory(xa);
        pooled.setMaxPoolSize(10);
        return pooled;
    }

    @Bean
    JmsTemplate jmsTemplate(ConnectionFactory cf) {
        JmsTemplate t = new JmsTemplate(cf);
        t.setSessionTransacted(true); // enlist in JTA rather than auto-ack
        return t;
    }
}

@Service
class OrderService {
    private final JdbcTemplate jdbc;
    private final JmsTemplate jms;
    OrderService(JdbcTemplate jdbc, JmsTemplate jms) { this.jdbc = jdbc; this.jms = jms; }

    @Transactional // JtaTransactionManager -> DB + JMS commit atomically (2PC)
    void placeOrder(Order o) {
        jdbc.update("INSERT INTO orders(id,total) VALUES (?,?)", o.id(), o.total());
        jms.convertAndSend("orders.created", o);
    }
}

go deeper

for a junior

Not expected to configure this; awareness that a starter enables it is enough.

for a middle

Can name the starter and the XA requirement for both resources.

for a senior

Wires XADataSource + XAConnectionFactory, transacted sessions, and knows recovery config (tx-log dir, node id) — the expected answer.

for a principal

Weighs operational cost (durable logs in k8s, node ids, provider maintenance) and often recommends outbox over XA for DB+broker.

## Overview of the moving parts For one transaction across a DB and a JMS broker you need three things wired: (1) a JTA **coordinator**, (2) each resource exposed and pooled as **XA**, (3) `JtaTransactionManager` as the Spring `PlatformTransactionManager`. Spring Boot's `JtaAutoConfiguration` glues these together when a provider starter is present. ## 1. Pick and add a provider - **Atomikos**: `spring-boot-starter-jta-atomikos` (embedded coordinator). Historically the most common Boot choice. - **Narayana**: `narayana-spring-boot-starter` (Red Hat's TM, also used in WildFly/Quarkus). - In a **Jakarta EE app server**, the container provides JTA and you just look up the container transaction manager — `JtaTransactionManager` auto-detects it. Adding the starter causes Boot to create a `JtaTransactionManager` bean; `@Transactional` then routes through it automatically. ## 2. Make the database XA A normal `DataSource` cannot enlist. You need: - a driver `javax.sql.XADataSource` implementation (`org.postgresql.xa.PGXADataSource`, MySQL's `MysqlXADataSource`, etc.), - wrapped by the provider's XA connection pool (Atomikos `AtomikosDataSourceBean`, or Narayana's pooled datasource). Boot can build this from `spring.datasource.xa.*` properties. The XA driver must be configured; some databases require enabling XA/prepared-transactions (e.g. Postgres `max_prepared_transactions > 0`). ## 3. Make JMS XA and transacted The JMS `ConnectionFactory` must be an `XAConnectionFactory` (e.g. ActiveMQ's `ActiveMQXAConnectionFactory`), wrapped in a pooling XA connection factory. The `JmsTemplate`/listener must use **transacted, non-auto-ack sessions** so the send/receive enlists in the global transaction. Under Boot JTA auto-config this is handled — `sessionTransacted` is driven by the JTA setup rather than local JMS transactions. ## 4. Coordinator durability & recovery The coordinator writes a **transaction log** used for XA recovery after crashes. You must configure: - a **stable log directory** on durable storage (Atomikos `spring.jta.atomikos.properties.log-base-dir`, Narayana object-store dir). In Kubernetes this means a persistent volume, not the ephemeral container FS. - a **unique transaction-manager id / node id** per instance (`spring.jta.transaction-manager-id`), otherwise two instances share/collide on recovery and can corrupt or double-resolve in-doubt transactions. ## 5. Use it Nothing special in code — a plain `@Transactional` method mixing JDBC/JPA and JMS is now global. Rollback rules, propagation, and timeouts behave as usual; a `JtaTransactionManager` also honors a global transaction timeout. ## Gotchas - **Every** participating resource must be XA; mixing one XA + one non-XA resource loses atomicity (unless you deliberately rely on last-resource-commit optimization, which is provider-specific and risky). - **Ephemeral containers + tx logs** are a real operational pain — losing the log strands in-doubt transactions. - **Atomikos maintenance / licensing** and the general decline of embedded XA push many teams toward **transactional outbox** instead of true XA for DB+broker. - With **JPA**, ensure the persistence context flushes before prepare — Boot's `JpaTransactionManager` is *not* used here; the `JtaTransactionManager` drives Hibernate through the JTA platform integration. - Don't set both a local `DataSourceTransactionManager`/`JpaTransactionManager` and JTA — Boot backs off local ones when JTA is active; overriding can silently give you local (non-global) transactions.

  • Why does the transaction-manager id need to be unique per instance, and why must the log directory be durable?
    The coordinator identifies its in-doubt transactions by node id in the tx log during XA recovery. Two instances sharing an id could each try to resolve the other's in-doubt transactions, causing double-resolution/corruption. If the log directory is on ephemeral storage and the container is replaced, the coordinator loses its record of prepared transactions and cannot recover them — resources stay blocked in-doubt.
  • What happens if one resource is XA and the other is a plain (non-XA) DataSource?
    You lose true atomicity. Only XA resources can participate in 2PC. Some coordinators offer a last-resource-commit optimization allowing exactly one non-XA resource, committing it in a 1PC step during phase two, but it narrows the safety guarantee and is provider-specific — generally avoid it.

saying these in an interview costs you the question

  • Believing a normal DataSource/ConnectionFactory can join a JTA transaction without XA wrapping.
  • Ignoring the transaction log durability / unique node id, which breaks recovery in containers.
  • Leaving JMS sessions auto-ack so the send never enlists in the global transaction.
  • Configuring a local JpaTransactionManager alongside JTA and assuming transactions are still global.

context