Skip to main content

10.4 Cross-API Transactions

One business operation can mix JdbcTemplate, Mapper, BaseMapper, and the builder API without starting a separate transaction for each API. For example, inserting an order with JdbcTemplate and reducing stock with the builder API can commit or roll back together.

Conditions for Sharing a Transaction

  • All APIs and the transaction manager use the same DataSource instance; matching connection URLs alone is not enough.
  • Operations execute within the transaction scope on the same thread. A new thread does not automatically join the transaction.
  • The database and driver support the required transaction operations. Cross-API transactions are not distributed transactions across data sources.

This applies to annotation-based, template, and programmatic transactions. Through thread context, dbVisitor lets each API reuse the current transaction connection; TransactionManager commits or rolls it back. Calls that open another transaction follow the configured propagation rules.

Interface Capabilities

MethodPurpose
begin()Begin a transaction; defaults to Propagation.REQUIRED + Isolation.DEFAULT
begin(Propagation)Specify propagation with default isolation
begin(Propagation, Isolation)Specify propagation and isolation
commit()Commit the most recent transaction
commit(TransactionStatus)Commit the specified transaction status
rollBack()Roll back the most recent transaction
rollBack(TransactionStatus)Roll back the specified transaction status
hasTransaction()Whether this manager has unfinished transactions
isTopTransaction(TransactionStatus)Whether the specified transaction is at the top of the stack

Each begin(...) returns a TransactionStatus describing that transaction scope:

MethodMeaning
getPropagation()Propagation for this transaction
getIsolationLevel()Isolation for this transaction
isCompleted()Whether it has committed or rolled back
isRollbackOnly()Whether marked for rollback
isReadOnly()Whether marked read-only
isNewConnection()Whether a new database connection was opened
isSuspend()Whether an existing transaction was suspended
hasSavepoint()Whether a savepoint was created
setRollback()Request rollback on commit
setReadOnly()Mark read-only; commit performs rollback

Local Transaction Manager

The default implementation is LocalTransactionManager:

Create a Local Transaction Manager
import net.hasor.dbvisitor.transaction.TransactionManager;
import net.hasor.dbvisitor.transaction.support.LocalTransactionManager;

TransactionManager txManager = new LocalTransactionManager(dataSource);

Usually, obtain the manager through TransactionHelper:

Reuse One Transaction Manager per DataSource
import net.hasor.dbvisitor.transaction.support.TransactionHelper;

TransactionManager txManager = TransactionHelper.txManager(dataSource);

LocalTransactionManager binds to one DataSource and reuses the current transaction connection through thread context. JdbcTemplate and Mapper operations accessing that DataSource inside a transaction obtain the connection bound to the current thread.

Transaction Stack

A manager can call begin repeatedly. Each call produces a TransactionStatus and pushes it onto the transaction stack.

Begin Three Transaction Scopes
TransactionStatus tranA = txManager.begin();
TransactionStatus tranB = txManager.begin();
TransactionStatus tranC = txManager.begin();

The stack looks like this:

Stack top
|
v
+--------+
| Tran C |
+--------+
| Tran B |
+--------+
| Tran A |
+--------+

Normally, finish in reverse order:

txManager.commit(tranC);
txManager.commit(tranB);
txManager.commit(tranA);

Committing tranA directly first processes transactions opened after it, then processes tranA:

Commit an Outer Scope
txManager.commit(tranA);

Equivalent to:

txManager.commit(tranC);
txManager.commit(tranB);
txManager.commit(tranA);

This avoids leaving entries on the stack, but explicitly committing or rolling back from the top remains clearer.

How Propagation Affects Connections

begin(Propagation, Isolation) decides whether to open, reuse, or suspend a connection or create a savepoint based on the current thread's transaction context.

PropagationWith an Existing TransactionConnection / Savepoint Effect
REQUIREDJoin existingReuse current connection; inner commit does not commit the database transaction
REQUIRES_NEWSuspend existing and begin newNew connection; restore outer connection on completion
NESTEDCreate nested scopeCreate a savepoint on the current connection
SUPPORTSJoin existingReuse current connection
NOT_SUPPORTEDSuspend existing and run non-transactionallyTemporarily clear the transaction connection
NEVERThrow an exceptionMust not run within a transaction
MANDATORYJoin existingThrow if no transaction exists

Commit, Rollback, and Read-Only Markers

TransactionTemplateManager and TransactionInterceptor follow the same rule: if TransactionStatus is marked for rollback or read-only, the final commit call performs rollback.

Call commit after Marking Rollback
TransactionStatus tran = txManager.begin();
try {
jdbcTemplate.executeUpdate(
"update sku_stock set quantity = quantity - ? where sku_id = ?",
new Object[] { quantity, skuId }
);

if (quantity <= 0) {
tran.setRollback();
}

txManager.commit(tran);
} catch (Throwable e) {
if (!tran.isCompleted()) {
try {
txManager.rollBack(tran);
} catch (Throwable rollbackError) {
e.addSuppressed(rollbackError);
}
}
throw e;
}

If quantity <= 0, commit(tran) follows the rollback path.

Relationship to the Three APIs

@Transactional
-> TransactionInterceptor
-> TransactionManager.begin(...)
-> TransactionManager.commit(...) or rollback

TransactionTemplate.execute(...)
-> TransactionTemplateManager
-> TransactionManager.begin(...)
-> TransactionManager.commit(...) or rollback

Programmatic Transactions
-> Business code calls TransactionManager directly

Define the business boundary with annotations, a template, or programmatic transactions, then mix APIs within it. No additional transaction bridging is required.