JTA transaction support for Quarkus applications with programmatic and declarative transaction management
—
Standard JTA @Transactional annotations with Quarkus-specific enhancements for timeout configuration and comprehensive CDI integration.
Standard Jakarta Transaction annotation for declarative transaction management.
/**
* Marks method or class for automatic transaction management
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Transactional {
/**
* Transaction propagation behavior
* @return TxType constant defining transaction semantics
*/
TxType value() default TxType.REQUIRED;
/**
* Exception types that trigger rollback
* @return Array of exception classes
*/
Class[] rollbackOn() default {};
/**
* Exception types that do NOT trigger rollback
* @return Array of exception classes
*/
Class[] dontRollbackOn() default {};
}TxType Values:
enum TxType {
/** Join existing transaction or create new one (default) */
REQUIRED,
/** Always create new transaction, suspend existing */
REQUIRES_NEW,
/** Must run within existing transaction, throw exception if none */
MANDATORY,
/** Run within transaction if exists, without transaction if none */
SUPPORTS,
/** Always run without transaction, suspend existing */
NOT_SUPPORTED,
/** Throw exception if transaction exists */
NEVER
}Usage Examples:
import jakarta.transaction.Transactional;
import static jakarta.transaction.Transactional.TxType.*;
@ApplicationScoped
public class UserService {
@Transactional // Uses REQUIRED by default
public void createUser(User user) {
validateUser(user);
userRepository.persist(user);
// Transaction committed automatically on success
// Rolled back automatically on RuntimeException
}
@Transactional(REQUIRES_NEW)
public void auditUserCreation(String username) {
// Always runs in new transaction, independent of caller
auditRepository.log("User created: " + username);
}
@Transactional(MANDATORY)
public void updateUserInTransaction(User user) {
// Must be called within existing transaction
user.setLastModified(Instant.now());
userRepository.merge(user);
}
@Transactional(NOT_SUPPORTED)
public void sendEmailNotification(String email, String message) {
// Runs outside transaction scope
emailService.send(email, message);
}
}Configure which exceptions cause rollback vs commit.
/**
* Control transaction rollback based on exception types
*/
@Transactional(
rollbackOn = {BusinessException.class, ValidationException.class},
dontRollbackOn = {WarningException.class}
)Usage Examples:
@ApplicationScoped
public class PaymentService {
// Roll back on any RuntimeException (default behavior)
@Transactional
public void processPayment(Payment payment) {
if (payment.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Invalid amount"); // Triggers rollback
}
paymentProcessor.charge(payment);
}
// Custom rollback behavior
@Transactional(
rollbackOn = {InsufficientFundsException.class},
dontRollbackOn = {PaymentWarningException.class}
)
public void processRiskyPayment(Payment payment) {
try {
paymentProcessor.processHighRisk(payment);
} catch (PaymentWarningException e) {
// Transaction continues and commits
logger.warn("Payment processed with warning", e);
} catch (InsufficientFundsException e) {
// Transaction is rolled back
throw e;
}
}
}Quarkus-specific annotation for configuring transaction timeouts.
/**
* Configure transaction timeout at method or class level
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface TransactionConfiguration {
/** Indicates no timeout configured */
int UNSET_TIMEOUT = -1;
/** Indicates no config property configured */
String UNSET_TIMEOUT_CONFIG_PROPERTY = "<<unset>>";
/**
* Transaction timeout in seconds
* @return Timeout value, UNSET_TIMEOUT for default
*/
int timeout() default UNSET_TIMEOUT;
/**
* Configuration property name for timeout value
* Property value takes precedence over timeout() if both are set
* @return Property name or UNSET_TIMEOUT_CONFIG_PROPERTY
*/
String timeoutFromConfigProperty() default UNSET_TIMEOUT_CONFIG_PROPERTY;
}Usage Examples:
@ApplicationScoped
public class BatchProcessingService {
@Transactional
@TransactionConfiguration(timeout = 300) // 5 minutes
public void processLargeBatch(List<BatchItem> items) {
for (BatchItem item : items) {
processItem(item);
}
}
@Transactional
@TransactionConfiguration(timeoutFromConfigProperty = "batch.processing.timeout")
public void configurableBatchProcess(List<BatchItem> items) {
// Timeout read from application.properties: batch.processing.timeout=600
processBatchItems(items);
}
@Transactional
@TransactionConfiguration(
timeout = 120, // Fallback value
timeoutFromConfigProperty = "critical.operation.timeout"
)
public void criticalOperation() {
// Uses property value if set, otherwise uses 120 seconds
performCriticalWork();
}
}Apply transaction behavior to all methods in a class.
/**
* Class-level annotations apply to all @Transactional methods
* Method-level configuration overrides class-level
*/
@Transactional(REQUIRES_NEW)
@TransactionConfiguration(timeout = 60)
@ApplicationScoped
public class AuditService {
// Inherits REQUIRES_NEW and 60s timeout
@Transactional
public void logUserAction(String action) { }
// Overrides to use REQUIRED semantics, keeps 60s timeout
@Transactional(REQUIRED)
public void logSystemEvent(String event) { }
// Overrides timeout to 30s, keeps REQUIRES_NEW semantics
@TransactionConfiguration(timeout = 30)
@Transactional
public void logQuickEvent(String event) { }
}Integration with CDI scopes and lifecycle events.
/**
* Transaction-scoped CDI beans
*/
@TransactionScoped
public class TransactionScopedAuditLogger {
private List<String> transactionLogs = new ArrayList<>();
@PostConstruct
void onTransactionBegin() {
// Called when transaction begins
transactionLogs.add("Transaction started at " + Instant.now());
}
@PreDestroy
void onTransactionEnd() {
// Called before transaction ends (commit or rollback)
persistLogs(transactionLogs);
}
public void log(String message) {
transactionLogs.add(message);
}
}Transaction Lifecycle Events:
@ApplicationScoped
public class TransactionEventObserver {
void onTransactionBegin(@Observes @Initialized(TransactionScoped.class) Object event) {
logger.info("Transaction scope initialized");
}
void onBeforeTransactionEnd(@Observes @BeforeDestroyed(TransactionScoped.class) Object event) {
logger.info("Transaction about to end");
}
void onTransactionEnd(@Observes @Destroyed(TransactionScoped.class) Object event) {
logger.info("Transaction scope destroyed");
}
}Understanding how transaction interceptors work with method calls.
@ApplicationScoped
public class OrderService {
@Transactional
public void processOrder(Order order) {
validateOrder(order); // Runs in same transaction
persistOrder(order); // Runs in same transaction
notifyCustomer(order); // Runs in same transaction
}
@Transactional(REQUIRES_NEW)
private void auditOrderProcessing(Order order) {
// Private method - interceptor NOT applied!
// This will NOT start a new transaction
auditRepository.log("Processing order: " + order.getId());
}
@Transactional(REQUIRES_NEW)
public void auditOrderProcessingPublic(Order order) {
// Public method - interceptor applied
// This WILL start a new transaction
auditRepository.log("Processing order: " + order.getId());
}
}// ✅ Correct - public method, interceptor applies
@Transactional
public void processData() { }
// ❌ Wrong - private method, interceptor NOT applied
@Transactional
private void processDataPrivate() { }
// ✅ Correct - package-private works with CDI
@Transactional
void processDataPackage() { }@ApplicationScoped
public class DocumentService {
@Transactional
public void processDocument(Document doc) {
validateDocument(doc);
// ❌ Self-invocation - transaction interceptor NOT applied
this.saveDocument(doc);
}
@Transactional(REQUIRES_NEW)
public void saveDocument(Document doc) {
// This will NOT start new transaction when called from processDocument
documentRepository.save(doc);
}
}
// ✅ Solution: Inject self or use separate service
@ApplicationScoped
public class DocumentService {
@Inject
DocumentService self; // CDI proxy
@Transactional
public void processDocument(Document doc) {
validateDocument(doc);
// ✅ Correct - uses CDI proxy, interceptor applies
self.saveDocument(doc);
}
}@Transactional
public void businessOperation() {
try {
riskyOperation();
} catch (CheckedException e) {
// Checked exceptions don't trigger rollback by default
// Convert to runtime exception to trigger rollback
throw new BusinessException("Operation failed", e);
}
}
@Transactional(rollbackOn = CheckedException.class)
public void businessOperationWithCheckedRollback() {
// Now CheckedException will trigger rollback
riskyOperationThatThrowsCheckedException();
}Install with Tessl CLI
npx tessl i tessl/maven-io-quarkus--quarkus-narayana-jta