Java & Spring Boot
Java & Spring Boot/senior/freq 5/5

Transactions & @Transactional Pitfalls

@Transactional is a proxy-based AOP advice. Self-invocation, checked exceptions, and propagation defaults silently break what looks like correct code.

springjpatransactions

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 production

A 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-level
Q1Why 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 @Transactional on 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.

Related