Annotation Interface Transactional
When a method annotated with @Transactional is executed, it will automatically be wrapped in a database transaction. The transaction will be committed if the method completes normally, or rolled back if an exception is thrown.
A method that joins a transaction already active on the thread (Propagation.REQUIRED,
the default, or Propagation.SUPPORTS or Propagation.MANDATORY called inside a
transaction) does not end it. When such a method throws an exception its
rollbackFor()/noRollbackFor() rules accept, the whole transaction is marked
rollback-only, even if the caller catches the exception: the outermost commit then rolls back
every write of the transaction and throws
UnexpectedRollbackException. An exception the rules do
not accept leaves the transaction committable. This is Spring's default
(AbstractPlatformTransactionManager, globalRollbackOnParticipationFailure = true),
and it applies on every backend (SQLite, MySQL and JSON).
Usage examples:
// Basic usage - starts a new transaction or joins existing one
@Transactional
public void saveUser(User user) {
userRepository.save(user);
auditRepository.log("User saved: " + user.getName());
}
// Always create a new transaction
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAudit(String message) {
auditRepository.log(message);
}
// Read-only transaction (may allow optimizations)
@Transactional(readOnly = true)
public List<User> findAllUsers() {
return userRepository.findAll();
}
// Custom rollback rules
@Transactional(rollbackFor = {BusinessException.class})
public void processOrder(Order order) throws BusinessException {
// ...
}
Important:
- Self-invocation (calling a
@Transactionalmethod onthisfrom within the same class) is intercepted: the framework's generated proxy is a subclass of the bean itself, not a delegate wrapping a separate target, so a call tothis.method()dispatches virtually onto the proxy's override. Re-verified against three pre-existingshouldInterceptSelfInvocationtest classes (2026-08-27) - Private, static, and final methods cannot be transactional - each is dispatched in a way
that bypasses the proxy (respectively:
invokespecial,invokestatic, and no override is possible); a package-private method is also ineligible when it is declared in a different package than the bean class - The class must not be final (subclass proxy limitation)
- On the JSON backend (
datasource.type: json), table locks belong to the thread: aPropagation.REQUIRES_NEWorPropagation.NOT_SUPPORTEDmethod re-enters the table locks its suspended caller holds instead of waiting for them, and if both write the same key, the caller's later commit is the value that stays. (On the JDBC backends the inner call would wait for its own caller's row lock until the lock-wait timeout.) - Work whose failure the caller wants to survive goes in a
Propagation.REQUIRES_NEWmethod, which commits or rolls back alone; a joined method's failure marks the caller's whole transaction rollback-only (see above).
- Since:
- 6.2.0
- Author:
- wisdomme
- See Also:
-
Optional Element Summary
Optional ElementsModifier and TypeOptional ElementDescriptionThe transaction isolation level.Exception types that should NOT trigger a rollback, overriding theRuntimeException/Errordefault for those types.The transaction propagation behavior.booleanWhether the transaction is read-only.Exception types that should trigger a rollback, in addition to theRuntimeException/Errordefault.intThe transaction timeout in seconds.
-
Element Details
-
propagation
Propagation propagationThe transaction propagation behavior.Defaults to
Propagation.REQUIRED, which joins an existing transaction or creates a new one if none exists.- Returns:
- the propagation behavior
- Default:
REQUIRED
-
isolation
Isolation isolationThe transaction isolation level.Defaults to
Isolation.DEFAULT, which uses the database's default isolation level.On the JSON backend, dirty writes and blind overwrites are prevented at every level by table write locks held from a transaction's first write to a table until it commits or rolls back; at the default level a read takes no lock, so a read-modify-write can lose an update: use
DataOperator#updateIforisolation = REPEATABLE_READorSERIALIZABLE, which lock the table at the first read.- Returns:
- the isolation level
- Default:
DEFAULT
-
timeout
int timeoutThe transaction timeout in seconds.Defaults to -1 (no timeout / use database default). Only a value greater than 0 is ever acted on at all -- the interceptor skips calling
TransactionManager.setTimeout(int)entirely for -1 or 0, so both mean exactly "no timeout requested," on every backend.The bound is per statement against a shared, shrinking budget -- not a wall-clock limit on the method body. (D-10, Spring's approach.) When a value greater than 0 is set, the transaction gets a deadline
Nseconds out from when it began. Every individual JDBC statement issued afterward -- through the ORM's normal read/write methods, and through the direct batch paths ininsertAll/updateAll-- is given a query timeout equal to whatever time is left in that budget when the statement is prepared, floored at 1 second so an exhausted budget still fails fast rather than becoming unlimited (setQueryTimeout(0)means "no limit" in the JDBC contract). This is deliberately not a wall-clock bound on the method as a whole: non-database work inside the method (a slow computation, a network call to something else) is never interrupted, because plain JDBC has no mechanism to cancel work already in flight, and this framework does not add one (seeREQUIREMENTS.md's "out of scope" section).Per backend:
- SQLite, MySQL (JDBC-backed): enforced exactly as described above.
- JSON-backed: the same deadline,
Nseconds after the transaction began. Every data operation inside the transaction checks it before it starts and fails with aDataAccessExceptiononce it has passed, and every wait for another transaction's table lock ends at the deadline if that comes before the 5-second lock-wait bound. Either failure marks the transaction rollback-only before it is thrown, as Spring'sResourceHolderSupport#checkTransactionTimeoutdoes, so a method that catches it still ends in a rollback andUnexpectedRollbackException.
setTimeoutfor it.- Returns:
- the timeout in seconds
- Default:
-1
-
readOnly
boolean readOnlyWhether the transaction is read-only.A read-only transaction may allow database optimizations and is useful for methods that only query data.
On the JSON backend every write inside a read-only transaction is rejected with a
DataAccessExceptionbefore anything is staged or locked.- Returns:
- true if read-only
- Default:
false
-
rollbackFor
Exception types that should trigger a rollback, in addition to theRuntimeException/Errordefault.This is additive, not a replacement: an exception that matches neither
rollbackFornornoRollbackForstill falls through to the unchecked default, exactly as if neither attribute were set. When an exception matches both this andnoRollbackFor(), the rule whose listed class is the shallower inheritance-depth match to the thrown exception wins (Spring'sRuleBasedTransactionAttributetiebreak); on an exact-depth tie - including the same class appearing in both arrays - the transaction rolls back.For a method that joins an existing transaction, these rules decide whether its exception marks that transaction rollback-only.
- Returns:
- exception types to additionally rollback for
- Since:
- 6.3.0 additive combination with
noRollbackFor()and the depth tiebreak (D-06, D-07); before 6.3.0 a non-empty, unmatchedrollbackForsilently committed instead of falling through to the default.
- Default:
{}
-
noRollbackFor
Exception types that should NOT trigger a rollback, overriding theRuntimeException/Errordefault for those types.Combines with
rollbackFor()by shallowest-inheritance-depth match, described there; on an exact-depth tie the transaction still rolls back.For a method that joins an existing transaction, these rules decide whether its exception marks that transaction rollback-only: an exception listed here leaves it committable.
- Returns:
- exception types to not rollback for
- Default:
{}
-