@Transactional is AOP advice, with the same self-invocation caveat as any other aspect — plus every propagation type, isolation override, rollback rule, and the readOnly hint's real effect.
Published September 23, 2026
As established in Spring AOP, @Transactional is implemented as an @Around-style aspect matching @annotation(Transactional) — meaning the exact same self-invocation caveat applies: calling a @Transactional method on this from within the same class bypasses the proxy entirely, and the transaction never actually begins. This is worth restating specifically for @Transactional, because it's the single most common place this bug bites people in practice — a method that "should" be transactional silently isn't, with no exception, no warning, just data written outside any transaction boundary.
@Transactional(propagation = Propagation.REQUIRED) // default
void outer() { inner(); } // if outer() is already in a transaction, inner() joins it
@Transactional(propagation = Propagation.REQUIRES_NEW)
void inner() { ... } // suspends any existing transaction, starts a completely independent new one
REQUIRED (default) — join the existing transaction if one is active; start a new one if not.REQUIRES_NEW — always suspend any existing transaction and start a fresh, independent one — useful when a piece of work (e.g. an audit log write) must commit even if the outer transaction later rolls back.NESTED — starts a true nested transaction with its own savepoint, rollback of which only undoes the nested portion, not the outer transaction (support varies by underlying datastore/driver — not universally available).SUPPORTS — join a transaction if one exists; run without one otherwise (no independent behavior either way).MANDATORY — must be called within an existing transaction; throws an exception if there isn't one.NOT_SUPPORTED — suspends any existing transaction and runs without one.NEVER — throws an exception if called within an existing transaction at all.REQUIRED covers the overwhelming majority of real cases; REQUIRES_NEW is the one worth understanding deeply, since "this specific side effect needs to survive even if the caller rolls back" is a genuinely common real requirement.
@Transactional(isolation = Isolation.READ_COMMITTED)
Overrides the database's default isolation level for this specific transaction — useful when a particular operation needs stronger (or, more rarely, weaker) consistency guarantees than the rest of the application's default, without changing the database-wide configuration for every other transaction.
@Transactional // by DEFAULT, rolls back on RuntimeException and Error — NOT on checked exceptions
void transferFunds() throws InsufficientFundsException { // a checked exception
debit();
credit(); // if this throws InsufficientFundsException, the transaction COMMITS anyway by default!
}
@Transactional(rollbackFor = InsufficientFundsException.class) // explicitly widen the rollback rule
void transferFundsFixed() throws InsufficientFundsException { ... }
This default is a genuinely common source of production bugs: Spring's default rollback rule only covers unchecked exceptions (RuntimeException and its subclasses, plus Error) — a checked exception thrown from a @Transactional method does not trigger a rollback unless explicitly configured via rollbackFor. noRollbackFor does the reverse — exclude a specific exception type from triggering rollback even though it would otherwise (e.g. a RuntimeException subtype that represents an expected, non-fatal condition).
@Transactional(readOnly = true)
List<Order> findAllOrders() { ... }
This tells the underlying persistence provider "no writes are expected in this transaction," which can enable real optimizations (skipping dirty-checking overhead in an ORM, routing to a read replica in some database setups) — but it is a hint, not a hard constraint. Depending on the driver/provider, an actual write inside a readOnly = true transaction might succeed anyway, fail, or behave inconsistently — it's not a reliable enforcement mechanism for "this method must not write," only a performance optimization signal.
@Transactional(timeout = 10) // seconds
If the transaction hasn't committed within the specified duration, it's forcibly rolled back — a safety net against a transaction left open indefinitely (e.g. due to a slow downstream call or a bug), which would otherwise hold database locks and resources far longer than intended.
Q: Why does REQUIRES_NEW suspend the outer transaction instead of just running alongside it? A: A database connection typically can't participate in two transactions simultaneously — suspending the outer transaction (releasing its connection/resources temporarily) is what makes it possible to open a genuinely separate transaction on its own connection, then resume the outer transaction once the inner one completes.
Q: If the default rollback behavior only covers unchecked exceptions, why doesn't Spring just default rollbackFor to Exception.class (covering everything)? A: This mirrors Java's own checked-vs-unchecked exception philosophy: checked exceptions are meant to represent expected, recoverable conditions the caller is expected to handle explicitly, while unchecked exceptions represent unexpected failures — Spring's default treats a checked exception as "the caller is handling this, don't assume it's transaction-fatal," which is a deliberate design choice aligned with that philosophy, even though it surprises developers coming from a mental model where any exception should obviously roll back.
Q: How does @Transactional's self-invocation bug typically get caught in practice, given it fails silently?
A: Usually only via a specific, reproducible bug report ("why did this data get written even though an error occurred") or a deliberate code review checklist item — it's exactly the kind of bug that passes all normal testing if tests don't specifically assert on transactional rollback behavior, which is why some teams add an explicit lint/architecture rule flagging @Transactional methods called via this. within the same class.
Q: Does readOnly=true prevent a transaction from acquiring write locks? A: Not inherently by the JPA/Spring specification — actual behavior (whether it optimizes lock acquisition, routes to a replica, or has no effect at all) depends entirely on the specific database driver and persistence provider's interpretation of the hint, which is exactly why it should be treated as a possible optimization, not a correctness guarantee.