Jakarta Transactions (JTA)

Managing transactions in enterprise applications

← Back to Index

What are Transactions?

Think of a transaction like a bank transfer:

Jakarta Transactions (JTA) provides a standard API for managing transactions across multiple resources (databases, message queues, etc.) in enterprise applications.

ACID Properties

Property Meaning Example
Atomicity All or nothing Both withdraw and deposit succeed, or both fail
Consistency Valid state transitions Total money in system stays the same
Isolation Concurrent transactions don't interfere Two transfers at same time don't corrupt data
Durability Committed changes persist After commit, data survives server crash

Transaction Management Approaches

1. Container-Managed Transactions (CMT) - Recommended

import jakarta.ejb.*;
import jakarta.transaction.*;

@Stateless
public class BankService {

    @Inject
    private AccountRepository accountRepo;

    // Container manages the transaction automatically!
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId);
        Account to = accountRepo.findById(toId);

        from.withdraw(amount);  // If this fails...
        to.deposit(amount);     // ...this won't happen either!

        accountRepo.save(from);
        accountRepo.save(to);

        // Transaction commits automatically when method returns
        // Transaction rolls back if exception is thrown
    }
}

2. Bean-Managed Transactions (BMT)

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class ManualTransactionService {

    @Resource
    private UserTransaction userTransaction;

    @Inject
    private AccountRepository accountRepo;

    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        try {
            // Start transaction manually
            userTransaction.begin();

            Account from = accountRepo.findById(fromId);
            Account to = accountRepo.findById(toId);

            from.withdraw(amount);
            to.deposit(amount);

            accountRepo.save(from);
            accountRepo.save(to);

            // Commit manually
            userTransaction.commit();

        } catch (Exception e) {
            try {
                // Rollback on error
                userTransaction.rollback();
            } catch (Exception ex) {
                // Handle rollback error
            }
            throw new RuntimeException("Transfer failed", e);
        }
    }
}

Transaction Attributes (CMT)

@Stateless
public class TransactionDemo {

    // REQUIRED (default): Join existing or create new
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void required() {
        // If caller has transaction: join it
        // If no transaction: create new one
    }

    // REQUIRES_NEW: Always create new transaction
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void requiresNew() {
        // Suspend caller's transaction (if any)
        // Create new independent transaction
        // Good for: audit logs that must persist even if main tx fails
    }

    // MANDATORY: Must have existing transaction
    @TransactionAttribute(TransactionAttributeType.MANDATORY)
    public void mandatory() {
        // If caller has transaction: join it
        // If no transaction: throw exception!
    }

    // SUPPORTS: Optional transaction
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public void supports() {
        // If caller has transaction: join it
        // If no transaction: run without transaction
    }

    // NOT_SUPPORTED: Never use transaction
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public void notSupported() {
        // Suspend caller's transaction (if any)
        // Run without transaction
        // Good for: read-only operations, external calls
    }

    // NEVER: Must NOT have transaction
    @TransactionAttribute(TransactionAttributeType.NEVER)
    public void never() {
        // If caller has transaction: throw exception!
        // If no transaction: run without transaction
    }
}
Attribute No Tx Exists Tx Exists Use Case
REQUIRED Create new Join Default, most methods
REQUIRES_NEW Create new Create new (suspend) Audit logs, independent ops
MANDATORY Error Join Force caller to have tx
SUPPORTS No tx Join Optional tx support
NOT_SUPPORTED No tx No tx (suspend) Read-only, external calls
NEVER No tx Error Must not have tx

CDI with @Transactional

import jakarta.transaction.Transactional;
import jakarta.transaction.Transactional.TxType;

@ApplicationScoped
public class OrderService {

    @Inject
    private OrderRepository orderRepo;

    @Inject
    private PaymentService paymentService;

    @Inject
    private InventoryService inventoryService;

    // Equivalent to TransactionAttributeType.REQUIRED
    @Transactional(TxType.REQUIRED)
    public Order createOrder(OrderRequest request) {
        // All these operations are in the same transaction
        Order order = new Order(request);
        orderRepo.save(order);

        paymentService.processPayment(order);
        inventoryService.reserveItems(order);

        return order;
        // Commit on success, rollback on exception
    }

    @Transactional(TxType.REQUIRES_NEW)
    public void logOrderAttempt(OrderRequest request) {
        // Logs in separate transaction
        // Persists even if main order fails
    }

    // Specify which exceptions cause rollback
    @Transactional(
        rollbackOn = {PaymentException.class},
        dontRollbackOn = {ValidationWarning.class}
    )
    public void processPayment(Order order) {
        // PaymentException: rolls back
        // ValidationWarning: commits anyway
    }
}
Important!

@Transactional only works on CDI beans when called from outside the bean. Calling a transactional method from within the same class won't start a new transaction!

@ApplicationScoped
public class BrokenExample {

    @Transactional
    public void methodA() {
        // Transaction started
        methodB();  // NO new transaction! Same class call bypasses proxy
    }

    @Transactional(TxType.REQUIRES_NEW)
    public void methodB() {
        // Expected new transaction, but didn't get one!
    }
}

// Solution: Use self-injection
@ApplicationScoped
public class FixedExample {

    @Inject
    private FixedExample self;  // Self-inject the proxy

    @Transactional
    public void methodA() {
        self.methodB();  // Now goes through proxy, new tx created!
    }

    @Transactional(TxType.REQUIRES_NEW)
    public void methodB() {
        // Correctly runs in new transaction
    }
}

Rollback Handling

Automatic Rollback (Runtime Exceptions)

@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    Account from = accountRepo.findById(fromId);
    Account to = accountRepo.findById(toId);

    from.withdraw(amount);

    if (amount.compareTo(new BigDecimal("10000")) > 0) {
        // RuntimeException = automatic rollback
        throw new IllegalArgumentException("Amount too large");
    }

    to.deposit(amount);
    // If we get here, transaction commits
}

Manual Rollback

@Stateless
public class OrderService {

    @Resource
    private SessionContext ctx;  // EJB context

    public void createOrder(OrderRequest request) {
        try {
            // ... order logic

            if (!validateOrder(request)) {
                // Mark transaction for rollback
                ctx.setRollbackOnly();
                return;  // Transaction will rollback when method ends
            }

        } catch (Exception e) {
            ctx.setRollbackOnly();
            throw e;
        }
    }
}

Programmatic Rollback Check

@Inject
private TransactionManager tm;

public void someMethod() {
    try {
        // Check if current transaction is marked for rollback
        if (tm.getStatus() == Status.STATUS_MARKED_ROLLBACK) {
            // Don't do expensive operations, tx will rollback anyway
            return;
        }

        // Continue with operations...

    } catch (SystemException e) {
        // Handle error
    }
}

Distributed Transactions (XA)

// XA transactions span multiple resources (databases, JMS, etc.)

@Stateless
public class DistributedService {

    @PersistenceContext(unitName = "ordersDB")
    private EntityManager ordersEm;

    @PersistenceContext(unitName = "inventoryDB")
    private EntityManager inventoryEm;

    @Inject
    @JMSConnectionFactory("java:/JmsXA")
    private JMSContext jmsContext;

    // Single transaction across 2 databases and JMS!
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void processOrder(Order order) {
        // Write to orders database
        ordersEm.persist(order);

        // Write to inventory database
        InventoryItem item = inventoryEm.find(InventoryItem.class, order.getItemId());
        item.decreaseStock(order.getQuantity());

        // Send JMS message
        jmsContext.createProducer().send(queue, "Order created: " + order.getId());

        // All 3 operations commit or rollback together!
    }
}
Two-Phase Commit (2PC)

XA transactions use 2PC protocol:

  1. Prepare Phase: All resources confirm they can commit
  2. Commit Phase: If all say yes, all commit; if any says no, all rollback

Transaction Timeout

// Set timeout for long-running transactions

@Stateless
public class BatchService {

    @Resource
    private UserTransaction utx;

    public void processBatch() throws Exception {
        // Set timeout to 5 minutes (300 seconds)
        utx.setTransactionTimeout(300);
        utx.begin();

        try {
            // Long-running batch operation
            processManyRecords();
            utx.commit();
        } catch (Exception e) {
            utx.rollback();
            throw e;
        }
    }
}

// Or use annotation (container-specific)
@Stateless
@TransactionTimeout(value = 5, unit = TimeUnit.MINUTES)
public class SlowService {
    // All methods have 5-minute timeout
}

JPA with Transactions

@Stateless
public class ProductService {

    @PersistenceContext
    private EntityManager em;

    // CMT: transaction managed by container
    public void createProduct(Product product) {
        em.persist(product);
        // Changes flushed and committed when method returns
    }

    public void updateProduct(Long id, String newName) {
        Product p = em.find(Product.class, id);
        p.setName(newName);
        // No em.merge() needed! Entity is managed, changes auto-tracked
    }

    // Force immediate write to database
    public void createAndFlush(Product product) {
        em.persist(product);
        em.flush();  // Write to DB now (still in transaction)

        // product.getId() now available
        System.out.println("Created with ID: " + product.getId());
    }

    // Read-only optimization
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public List<Product> findAll() {
        return em.createQuery("SELECT p FROM Product p", Product.class)
                 .getResultList();
    }
}

Optimistic Locking

@Entity
public class Product {
    @Id
    private Long id;

    private String name;

    @Version  // Optimistic locking
    private Long version;
}

@Stateless
public class ProductService {

    @PersistenceContext
    private EntityManager em;

    public void updateProduct(Long id, String newName) {
        Product p = em.find(Product.class, id);
        p.setName(newName);
        // If another transaction modified this product,
        // OptimisticLockException is thrown on commit!
    }

    public void safeUpdate(Long id, String newName) {
        try {
            Product p = em.find(Product.class, id);
            p.setName(newName);
            em.flush();
        } catch (OptimisticLockException e) {
            // Handle concurrent modification
            throw new ConcurrentModificationException("Product was modified");
        }
    }
}

Common Patterns

Saga Pattern (for Microservices)

// When XA transactions aren't possible (microservices)

@ApplicationScoped
public class OrderSaga {

    @Inject
    private OrderService orderService;

    @Inject
    private PaymentService paymentService;

    @Inject
    private InventoryService inventoryService;

    public void createOrder(OrderRequest request) {
        String orderId = null;
        String paymentId = null;

        try {
            // Step 1: Create order
            orderId = orderService.create(request);

            // Step 2: Process payment
            paymentId = paymentService.charge(request.getPaymentInfo());

            // Step 3: Reserve inventory
            inventoryService.reserve(request.getItems());

            // Step 4: Complete order
            orderService.complete(orderId);

        } catch (Exception e) {
            // Compensating transactions (rollback)
            if (paymentId != null) {
                paymentService.refund(paymentId);
            }
            if (orderId != null) {
                orderService.cancel(orderId);
            }
            throw new OrderFailedException("Order creation failed", e);
        }
    }
}

Audit Logging (Separate Transaction)

@ApplicationScoped
public class AuditService {

    @PersistenceContext
    private EntityManager em;

    // Always log, even if main transaction fails
    @Transactional(TxType.REQUIRES_NEW)
    public void logAction(String user, String action, String details) {
        AuditLog log = new AuditLog();
        log.setUser(user);
        log.setAction(action);
        log.setDetails(details);
        log.setTimestamp(Instant.now());
        em.persist(log);
        // Commits in separate transaction
    }
}

@ApplicationScoped
public class AccountService {

    @Inject
    private AuditService auditService;

    @Transactional
    public void transferMoney(String user, Long fromId, Long toId, BigDecimal amount) {
        // Log attempt (persists even if transfer fails)
        auditService.logAction(user, "TRANSFER_ATTEMPT",
            "From: " + fromId + ", To: " + toId + ", Amount: " + amount);

        try {
            // Do transfer...
            doTransfer(fromId, toId, amount);

            auditService.logAction(user, "TRANSFER_SUCCESS", "Completed");
        } catch (Exception e) {
            auditService.logAction(user, "TRANSFER_FAILED", e.getMessage());
            throw e;
        }
    }
}

Best Practices

DO:

  • Use CMT by default - Let the container manage transactions
  • Keep transactions short - Long transactions cause locking issues
  • Use REQUIRES_NEW for audit logs - Must persist even on failure
  • Use optimistic locking (@Version) - Better scalability
  • Handle OptimisticLockException - Retry or notify user
  • Set appropriate timeouts - Prevent hung transactions
  • Use NOT_SUPPORTED for read-only - Better performance

DON'T:

  • Don't catch and ignore exceptions - Transaction won't rollback
  • Don't do external calls in transactions - HTTP, file I/O, etc.
  • Don't hold transactions during user input - Causes blocking
  • Don't forget about lazy loading - May fail after tx closes
  • Don't mix CMT and BMT - Confusing and error-prone
  • Don't use SERIALIZABLE isolation unless necessary - Performance impact

Summary

  • JTA: Standard API for transaction management
  • CMT: Container-Managed Transactions (recommended)
  • BMT: Bean-Managed Transactions (manual control)
  • @Transactional: CDI annotation for transaction demarcation
  • REQUIRED: Join or create transaction (default)
  • REQUIRES_NEW: Always new transaction (audit logs)
  • NOT_SUPPORTED: No transaction (read-only)
  • XA Transactions: Distributed transactions across resources
  • @Version: Optimistic locking in JPA
  • Saga Pattern: Compensating transactions for microservices