JDBC Transactions
Database transactions are one of the most critical features for ensuring data integrity and consistency in your applications. BoxLang provides comprehensive transaction support through both a modern transaction{} block syntax and the underlying bx:transaction component.
🚀 Why Transactions Matter
Transactions ensure the ACID properties of database operations:
⚛️ Atomicity: All operations succeed together or fail together - no partial updates
🔒 Consistency: Database remains in a valid state before and after the transaction
🏝️ Isolation: Concurrent transactions don't interfere with each other
💾 Durability: Committed changes persist even after system failures
💡 Real-World Examples
// ❌ Without transactions - DANGEROUS!
queryExecute( "UPDATE accounts SET balance = balance - 100 WHERE id = 1" );
// 💥 What if the application crashes here?
queryExecute( "UPDATE accounts SET balance = balance + 100 WHERE id = 2" );
// ✅ With transactions - SAFE!
transaction {
queryExecute( "UPDATE accounts SET balance = balance - 100 WHERE id = 1" );
queryExecute( "UPDATE accounts SET balance = balance + 100 WHERE id = 2" );
// Both updates succeed together or both are rolled back
}📝 Transaction Syntax
BoxLang offers multiple ways to work with transactions:
🎯 Block Syntax (Recommended)
The modern transaction{} block syntax provides automatic transaction management:
⚙️ Manual Transaction Control
For complex scenarios, use transaction BIFs directly:
🔧 Transaction Attributes & Options
📋 Available Attributes
action
begin, commit, rollback, setsavepoint
Action to perform on the transaction
isolation
read_uncommitted, read_committed, repeatable_read, serializable
Transaction isolation level
savepoint
String
Name of the savepoint to create or rollback to
nested
Boolean
Whether this is a nested transaction (default: false)
datasource
String
Specific datasource name for the transaction
🔒 Isolation Levels Explained
Choose the right isolation level based on your concurrency and consistency needs:
🎯 Transaction Behavior
Connections
In BoxLang transactions, no connection is acquired until the first JDBC query is executed. Consider this transaction block:
This transaction is a no-op. It begins, tries to set a savepoint, then roll back to the savepoint, then commit... but never ran any JDBC queries. Hence, every transactional BIF called above does exactly nothing (besides emit events).
🗄️ Datasources
In BoxLang, transactions are inherently single-connection concepts. You can't have a transaction that spans multiple connections or datasources.
Hence, despite containing TWO queries this transaction has only a single query that executes within a transaction context:
In Adobe and Lucee, the first query executed within the datasource determines the transactional datasource; that is, the first query to run sets the datasource to use for that transaction. Any queries which specify a different datasource will execute outside the context of the transaction.
To improve expectations around this behavior, BoxLang supports a datasource attribute on the transaction block:
Setting the datasource at the transaction block makes it much more obvious which datasource the transaction will operate upon.
💾 Savepoints
Savepoints allow you to create checkpoints within a transaction for partial rollbacks:
🚨 Exception Handling
Transactions automatically roll back when exceptions occur:
📊 Performance Considerations
⚡ Best Practices
🔄 Deadlock Prevention
🔄 Transaction Events
See transaction events for a list of events that are triggered during transaction lifecycles such as begin, commit, rollback, and savepoint operations.
🏗️ Nested Transactions
BoxLang fully supports nested or "child" transactions. Nested transactions use the same database connection as the parent transaction, which means queries will run on the same datasource as the parent, using the same connection parameters, and can be rolled back partially or in whole as the parent issues transactionRollback() statements.
To achieve all this, BoxLang transactions are savepoint-driven. All savepoints created (and referenced) within child transactions are prefixed within a unique ID to prevent collision. For example, executing transactionSetSavepoint( 'insert' ) within a child transaction will under the hood create a CHILD_{UUID}_insert savepoint. Furthermore, when child transaction begins a CHILD_{UUID}_BEGIN savepoint is created which will be used as a rollback point if transactionRollback() is called with no savepoint parameter.
📋 Nested Transaction Behaviors
Rolling back the child transaction will roll back to the
CHILD_{UUID}_BEGINsavepoint.A transaction commit in the child transaction does not commit the transaction, but instead creates a
CHILD_{UUID}_COMMITsavepoint.Rolling back the (entire) parent transaction will roll back the child transaction.
Rolling back the parent transaction to a pre-child savepoint will roll back the entire child transaction.
📚 Examples
Check out a few examples to hammer home the behaviors of a nested transaction:
In this example, the 'BMW X3' insert is rolled back by the unqualified transactionRollback() call, but the 'Ford Fusion' insert in the parent transaction is still committed to the database when the parent transaction completes:
Ford
Fusion
Note that we would get the same result if the child transaction threw an exception instead of rolling back:
Ford
Fusion
Let's run this same one again, but replace the child rollback with a commit, and add a rollback to the parent transaction:
You can see that regardless of the transactionCommit() in the child transaction, both inserts are rolled back:
🧰 Transactional BIFs
See our list of transactional BIFs:
isInTransaction()- Check if currently inside a transactiontransactionBegin()- Start a new transaction manuallytransactionCommit()- Commit the current transactiontransactionRollback()- Roll back the current transactiontransactionSetSavepoint()- Create a savepoint for partial rollbacks
🎨 Common Patterns
💰 Financial Transfers
🛒 E-commerce Order Processing
🔄 Batch Data Processing
💡 Pro Tip: The transaction{} block syntax automatically handles transaction lifecycle management, making it the preferred approach for most use cases. It maps directly to the bx:transaction component but provides cleaner, more readable code.
⚠️ Warning: Always keep transactions as short as possible to avoid blocking other operations. Long-running transactions can cause performance issues and deadlocks in high-concurrency applications.
Last updated
Was this helpful?
