Under The Hood
backend springboot Spring Framework 6.x / Spring Boot 3.x

@Transactional: Propagation, Isolation, and Rollback Rules

Last updated
Prerequisites:
Spring dependency injection basics
Understanding of database transactions (ACID)
  • spring
  • spring-boot
  • transactional
  • propagation
  • isolation
  • rollback
  • jdbc

Read at your depth

The practical view

@Transactional on a method makes Spring manage a transaction around it: it begins before the method, commits after success, and rolls back on failure. @Transactional(readOnly = true) is for queries. Propagation controls how it interacts with an existing transaction: REQUIRED (default — join or create), REQUIRES_NEW (suspend and create a new one), NESTED (savepoint), MANDATORY (must exist). Isolation sets the database isolation level (READ_COMMITTED, etc.). Rollback rules: by default, rollback only on RuntimeException/Error, NOT checked exceptions — configure with rollbackFor = Exception.class.

The same idea in other frameworks

react equivalent
N/A — server-side Spring concern.

Legacy vs modern

No transaction (per-statement autocommit) vs @Transactional

Without @Transactional each statement commits separately (partial writes on failure); the annotation makes the whole method one atomic unit.

before → after
Multiple writes without a transaction
void transfer(String from, String to, int amount) {
  accountRepo.debit(from, amount);  // ❌ OLD: commits immediately
  accountRepo.credit(to, amount);   // ❌ OLD: if this fails, debit stays
}
@Transactional atomic method
@Transactional
void transfer(String from, String to, int amount) { // ✅ NEW: one atomic unit
  accountRepo.debit(from, amount);
  accountRepo.credit(to, amount);
}

Interview gotchas

Context

This is the classic Spring transaction gotcha — interviewers want the proxy model explanation, because self-invocation silently skips the transaction interceptor.

The mechanical answer

@Transactional works through a proxy: Spring wraps the bean, and calls THROUGH the proxy get intercepted. When a method inside the same class calls another @Transactional method directly (this.someMethod()), the call goes to the raw target object, not through the proxy — so the transaction interceptor never runs, and the inner @Transactional is ignored (its transaction is not started/committed separately). This is why self-invocation breaks transactions, @Async, and other AOP-based annotations. Fixes: inject the bean into itself (self-injection to get the proxy), extract the method to another bean, use TransactionTemplate programmatically, or restructure so the call goes through the proxy. The transaction boundary is at the proxy, not the method.

Trap

The naive answer is 'Spring creates a transaction for every @Transactional method'. Only calls that pass through the proxy get intercepted. Another trap: assuming an inner call 'joins' the outer transaction automatically — it only does if it goes through the proxy AND the propagation is REQUIRED; self-invocation skips it entirely. Candidates who mention the proxy and self-injection fix show real Spring experience.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.