JTA transaction support for Quarkus applications with programmatic and declarative transaction management
—
Advanced transaction execution with customizable propagation behaviors, exception handling, timeout control, and lambda-style execution patterns.
Controls transaction behavior in the presence or absence of existing transactions.
/**
* Enum controlling transaction propagation behavior
*/
enum TransactionSemantics {
/**
* Throws exception if transaction already exists
* Otherwise creates new transaction
*/
DISALLOW_EXISTING,
/**
* Joins existing transaction or creates new one
* Exception handler affects existing transaction differently than new
*/
JOIN_EXISTING,
/**
* Always creates new transaction
* Suspends existing transaction if present, resumes after completion
*/
REQUIRE_NEW,
/**
* Suspends existing transaction and runs without transaction
* No-op if no existing transaction
* Exception handler cannot be used with this semantic
*/
SUSPEND_EXISTING
}Usage Examples:
import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.narayana.jta.TransactionSemantics;
@ApplicationScoped
public class OrderProcessingService {
public void processOrderWithAudit(Order order) {
// Main processing in existing or new transaction
QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING)
.run(() -> {
validateOrder(order);
updateInventory(order);
chargeCustomer(order);
});
// Audit logging in separate transaction
QuarkusTransaction.runner(TransactionSemantics.REQUIRE_NEW)
.run(() -> {
auditRepository.log("Order processed: " + order.getId());
});
}
public void sensitiveOperation() {
// Must run without any existing transaction
QuarkusTransaction.runner(TransactionSemantics.DISALLOW_EXISTING)
.run(() -> {
performSecurityCheck();
updateSecurityLog();
});
}
public void sendNotification(String message) {
// Run outside transaction scope entirely
QuarkusTransaction.runner(TransactionSemantics.SUSPEND_EXISTING)
.run(() -> {
emailService.send(message);
// This runs without any transaction, even if called
// from within a transactional method
});
}
}Base interface for executing tasks with transaction semantics.
/**
* Interface for running tasks with specific transaction semantics
*/
interface TransactionRunner {
/**
* Execute runnable with selected transaction semantics
* @param task Task to execute
*/
void run(Runnable task);
/**
* Execute callable with selected transaction semantics
* Checked exceptions are wrapped in QuarkusTransactionException
* @param task Task to execute
* @return Value returned by task
* @throws QuarkusTransactionException if task throws checked exception
*/
<T> T call(Callable<T> task);
}Enhanced transaction runner with configuration options.
/**
* Builder interface for configuring transaction runners
* Extends TransactionRunner so it can execute tasks directly
*/
interface TransactionRunnerOptions extends TransactionRunner {
/**
* Set transaction timeout for this execution
* @param seconds Timeout in seconds, 0 for system default
* @return This builder for method chaining
* @throws IllegalArgumentException if seconds is negative
*/
TransactionRunnerOptions timeout(int seconds);
/**
* Set exception handler for controlling commit/rollback behavior
* Handler is called when task throws exception to decide transaction fate
* Exception is still propagated to caller after handler executes
* Cannot be used with SUSPEND_EXISTING semantics
*
* @param handler Function returning COMMIT or ROLLBACK decision
* @return This builder for method chaining
*/
TransactionRunnerOptions exceptionHandler(
Function<Throwable, TransactionExceptionResult> handler
);
// Inherited from TransactionRunner
void run(Runnable task);
<T> T call(Callable<T> task);
}Usage Examples:
import io.quarkus.narayana.jta.TransactionExceptionResult;
@ApplicationScoped
public class PaymentProcessor {
public boolean processPayment(Payment payment) {
return QuarkusTransaction.requiringNew()
.timeout(30) // 30 second timeout
.exceptionHandler(throwable -> {
// Custom exception handling logic
if (throwable instanceof InsufficientFundsException) {
return TransactionExceptionResult.ROLLBACK;
} else if (throwable instanceof PaymentWarningException) {
return TransactionExceptionResult.COMMIT; // Continue despite warning
}
return TransactionExceptionResult.ROLLBACK; // Default to rollback
})
.call(() -> {
validatePayment(payment);
chargeCard(payment);
updateAccount(payment);
return true; // Success
});
}
public void performBatchOperation(List<Operation> operations) {
QuarkusTransaction.joiningExisting()
.timeout(300) // 5 minute timeout for batch
.run(() -> {
for (Operation op : operations) {
processOperation(op);
}
});
}
}Controls transaction outcome based on exception handling.
/**
* Decision enum for exception handler results
*/
enum TransactionExceptionResult {
/** Transaction should be committed despite exception */
COMMIT,
/** Transaction should be rolled back due to exception */
ROLLBACK
}Advanced Exception Handling:
@ApplicationScoped
public class DataMigrationService {
public void migrateDataWithRecovery(List<DataRecord> records) {
QuarkusTransaction.requiringNew()
.exceptionHandler(this::handleMigrationException)
.run(() -> {
for (DataRecord record : records) {
try {
migrateRecord(record);
} catch (DataCorruptionException e) {
logCorruptedRecord(record, e);
// Continue with other records
}
}
});
}
private TransactionExceptionResult handleMigrationException(Throwable throwable) {
if (throwable instanceof DataCorruptionException) {
// Log but don't fail entire batch
logger.warn("Data corruption detected", throwable);
return TransactionExceptionResult.COMMIT;
} else if (throwable instanceof DatabaseConnectionException) {
// Critical failure, rollback everything
logger.error("Database connection failed", throwable);
return TransactionExceptionResult.ROLLBACK;
} else if (throwable instanceof ValidationException) {
// Business logic error, rollback
return TransactionExceptionResult.ROLLBACK;
}
// Default to rollback for unknown exceptions
return TransactionExceptionResult.ROLLBACK;
}
}public void demonstrateJoinExisting() {
// Scenario 1: No existing transaction
QuarkusTransaction.joiningExisting().run(() -> {
// New transaction created
// Exception handler affects this transaction directly
performWork();
});
// Scenario 2: Called within existing transaction
QuarkusTransaction.begin();
try {
QuarkusTransaction.joiningExisting()
.exceptionHandler(ex -> TransactionExceptionResult.ROLLBACK)
.run(() -> {
// Runs in existing transaction
// ROLLBACK result marks existing transaction as rollback-only
// COMMIT result takes no action on existing transaction
performWork();
});
QuarkusTransaction.commit(); // May fail if marked rollback-only
} catch (Exception e) {
QuarkusTransaction.rollback();
}
}public void demonstrateRequireNew() {
QuarkusTransaction.begin();
try {
// This runs in the outer transaction
performInitialWork();
QuarkusTransaction.requiringNew().run(() -> {
// This runs in completely separate transaction
// Outer transaction is suspended
// Success/failure here doesn't affect outer transaction
performIndependentWork();
});
// Outer transaction resumed here
performFinalWork();
QuarkusTransaction.commit();
} catch (Exception e) {
QuarkusTransaction.rollback();
}
}public void demonstrateSuspendExisting() {
QuarkusTransaction.begin();
try {
performTransactionalWork();
QuarkusTransaction.suspendingExisting().run(() -> {
// Runs completely outside any transaction
// Cannot use exception handler with this semantic
sendEmailNotification();
logToExternalSystem();
});
// Original transaction resumed
performMoreTransactionalWork();
QuarkusTransaction.commit();
} catch (Exception e) {
QuarkusTransaction.rollback();
}
}@ApplicationScoped
public class LongRunningOperationService {
public void performQuickOperation() {
QuarkusTransaction.requiringNew()
.timeout(10) // 10 seconds
.run(() -> {
quickDatabaseUpdate();
});
}
public void performBatchOperation() {
QuarkusTransaction.requiringNew()
.timeout(600) // 10 minutes
.run(() -> {
processBatchRecords();
});
}
public void performOperationWithDefaultTimeout() {
QuarkusTransaction.requiringNew()
.timeout(0) // Use system default timeout
.run(() -> {
performStandardOperation();
});
}
}// ✅ Use REQUIRE_NEW for independent operations (auditing, logging)
QuarkusTransaction.requiringNew().run(() -> {
auditRepository.log("Operation completed");
});
// ✅ Use JOIN_EXISTING for operations that should be part of caller's transaction
QuarkusTransaction.joiningExisting().run(() -> {
updateRelatedData();
});
// ✅ Use SUSPEND_EXISTING for non-transactional operations (external APIs)
QuarkusTransaction.suspendingExisting().run(() -> {
callExternalAPI();
});
// ✅ Use DISALLOW_EXISTING for operations that must start fresh
QuarkusTransaction.disallowingExisting().run(() -> {
initializeSystemState();
});// ✅ Good: Clear exception handling logic
.exceptionHandler(throwable -> {
if (throwable instanceof BusinessWarningException) {
logger.warn("Warning during processing", throwable);
return TransactionExceptionResult.COMMIT;
}
return TransactionExceptionResult.ROLLBACK;
})
// ❌ Avoid: Side effects in exception handler
.exceptionHandler(throwable -> {
// Don't do heavy work in exception handler
sendEmailAlert(); // ❌ Bad
return TransactionExceptionResult.ROLLBACK;
})
// ✅ Good: Exception handler focuses on transaction decision only
.exceptionHandler(throwable -> {
// Keep it simple and fast
return throwable instanceof WarningException ?
TransactionExceptionResult.COMMIT :
TransactionExceptionResult.ROLLBACK;
})// ✅ Prefer declarative transactions for simple cases
@Transactional
public void simpleOperation() { }
// ✅ Use programmatic for complex logic
public void complexOperation() {
QuarkusTransaction.joiningExisting()
.timeout(customTimeout)
.exceptionHandler(this::handleComplexExceptions)
.run(() -> {
// Complex transaction logic
});
}Install with Tessl CLI
npx tessl i tessl/maven-io-quarkus--quarkus-narayana-jta