Transactions & @Transactional Pitfalls
@Transactional is a proxy-based AOP advice. Self-invocation, checked exceptions, and propagation defaults silently break what looks like correct code.
Deep dive
The proxy rule
@Transactional only works when the call goes through the Spring proxy. Calling a transactional method from another method in the same class bypasses the proxy — no transaction is started.
Rollback rule
By default Spring rolls back on RuntimeException and Error, not on checked exceptions. Add rollbackFor = Exception.class if you throw checked exceptions you want to roll back on.
Propagation
REQUIRED(default): join or create.REQUIRES_NEW: suspend outer, run in a new physical transaction (needs a new connection — watch your pool).NESTED: savepoint inside outer (JDBC only, not most JPA providers).
Isolation
Defaults to the DB's default (usually READ_COMMITTED on Postgres). Bump to REPEATABLE_READ or use SELECT ... FOR UPDATE when you need write-skew protection.
Real-world example
From productionA payments service occasionally double-charged customers on retry. Root cause: an outer @Transactional method called an inner "audit" method on the same bean. The audit threw, but because of self-invocation no rollback occurred — the charge committed, the audit row didn't, and the retry logic charged again. Fix: extracted the audit into a separate bean and switched to REQUIRES_NEW for the audit boundary.
Interview questions
2 senior-levelQ1Why doesn't @Transactional work when I call the method from the same class?▾
Spring's transaction support is implemented via a proxy. Self-invocation goes through this, not the proxy, so the advice never runs. Fix: move the method to another bean, use AopContext.currentProxy(), or switch to AspectJ weaving.
Q2When would you use REQUIRES_NEW?▾
When the inner operation must commit independently of the outer — typically audit logs, outbox writes, or compensating actions. Cost: an additional physical connection from the pool; size accordingly.
Common mistakes
Putting
@Transactionalon private methods (the proxy can't see them).Catching exceptions inside a transactional method and swallowing them — Spring never sees the rollback signal.
Long-running transactions that hold row locks while calling external HTTP services.
Trade-offs
Programmatic transactions (
TransactionTemplate) give precise boundaries but more boilerplate.Outbox pattern adds latency but is the only safe way to publish events alongside DB writes.