@Transactional Internals — Proxy and Propagation
Fast recall
Quick revision
Core answer
@Transactional is implemented entirely as a Spring AOP proxy: at startup, Spring wraps every @Transactional bean in a CGLIB subclass (or JDK dynamic proxy for interface-based beans). The proxy overrides each annotated method with code that opens a transaction via `PlatformTransactionManager.getTransaction()`, invokes the real method, and commits on success or rolls back on RuntimeException/Error. This proxy architecture explains every @Transactional gotcha — self-invocation silently skips the transaction (calling this.method() bypasses the proxy entirely), annotations on private/final/static methods are ignored, and only unchecked exceptions trigger rollback by default. Propagation levels (REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS, MANDATORY, NEVER, NOT_SUPPORTED) control how nested transactional calls combine; readOnly = true is a hint to Hibernate (skip dirty checking) and the JDBC driver (route to replicas).Deep understanding of Spring AOP and transaction management — not just how to annotate but what happens at runtime.
Calling a `@Transactional` method from within the same class bypasses the proxy and the transaction is silently skipped. Candidates often do not mention this self-invocation pitfall.
Explain the self-invocation pitfall, transaction synchronization, and how `readOnly = true` is a hint that lets Hibernate skip dirty checking and lets some JDBC drivers route to read replicas.
Interview-ready guide
Interview answer
The proxy mechanism
- Spring wraps every
@Transactionalbean in a CGLIB proxy at startup - The proxy begins a transaction, calls the real method, then commits or rolls back
this.method()bypasses the proxy — the transaction is silently skipped- Fix: move the called method to a separate
@Service, or self-inject the proxy
Propagation types
REQUIRED— join existing or create new; default; rollback in inner poisons the outerREQUIRES_NEW— suspends outer, creates a new transaction; uses a second DB connection; ideal for audit logsNESTED— savepoint inside outer; inner rollback returns to savepoint; needs JDBC savepoint supportSUPPORTS— joins if present, runs non-tx if notMANDATORY— requires an existing transaction
Rollback rules
RuntimeException/Error-> rollback by default- Checked exceptions -> commit unless
rollbackFor = Exception.class
`readOnly = true`
- Hint, not enforcement
- Hibernate skips dirty-check copies of loaded entities
- Some JDBC drivers route to read replicas
The classic trap — `UnexpectedRollbackException`
- Outer
REQUIREDcalls innerREQUIREDthat throws - Outer catches the exception and tries to continue
- At commit time Spring throws
UnexpectedRollbackExceptionbecause the transaction was marked rollback-only - Fix: make inner
REQUIRES_NEW, or do not catch the exception
@Transactional Internals — Proxy and Propagation
Direct Answer
@Transactional is implemented entirely as a Spring AOP proxy: at startup, Spring wraps every @Transactional bean in a CGLIB subclass (or JDK dynamic proxy for interface-based beans). The proxy overrides each annotated method with code that opens a transaction via PlatformTransactionManager.getTransaction(), invokes the real method, and commits on success or rolls back on RuntimeException/Error. This proxy architecture explains every @Transactional gotcha — self-invocation silently skips the transaction (calling this.method() bypasses the proxy entirely), annotations on private/final/static methods are ignored, and only unchecked exceptions trigger rollback by default. Propagation levels (REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS, MANDATORY, NEVER, NOT_SUPPORTED) control how nested transactional calls combine; readOnly = true is a hint to Hibernate (skip dirty checking) and the JDBC driver (route to replicas).
Code Example
@Transactional works via a Spring AOP proxy. The proxy consults the PlatformTransactionManager, opens/commits/rolls back a transaction around the method, and uses the Propagation attribute to decide whether to join an existing transaction or start a new one.
@Service
public class OrderService {
@Transactional // PROPAGATION_REQUIRED (default)
public void placeOrder(Order o) {
orderRepo.save(o);
auditService.record(o); // joins same transaction
notificationService.notify(o); // see below
}
}
@Service
class NotificationService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // suspends outer, starts new
public void notify(Order o) {
notificationRepo.save(...); // commits independently of outer
}
}
// --- The classic gotcha: self-invocation bypasses the proxy ---
@Service
public class BadService {
@Transactional public void outer() { this.inner(); } // this = raw bean, NOT proxy
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { /* runs in OUTER's tx, not a new one — proxy skipped! */ }
}
// --- Rollback rules: only RuntimeException triggers rollback by default ---
@Transactional(rollbackFor = Exception.class) // opt in to checked exceptions
public void transferFunds() throws IOException { ... }
| Propagation | Existing tx | No existing tx |
|---|---|---|
REQUIRED (default) | join | create new |
REQUIRES_NEW | suspend outer, create new | create new |
NESTED | savepoint inside outer | create new |
SUPPORTS | join | run without tx |
MANDATORY | join | throw |
NEVER | throw | run without tx |
- The proxy exists on the public method of a Spring-managed bean. Calling from inside the same class (
this.method()) bypasses it — extract to a separate bean or useAopContext.currentProxy()+@EnableAspectJAutoProxy(exposeProxy=true). REQUIRES_NEWcommits to the DB before the outer transaction commits, so partial work is durable even if the outer rolls back — use it for audit logs and outbox writes.- Checked exceptions do not roll back by default. Add
rollbackFor = Exception.classor throw aRuntimeException.
Common Mistakes & Pitfalls
Calling a @Transactional method from within the same class bypasses the proxy and the transaction is silently skipped. Candidates often do not mention this self-invocation pitfall.
Check your understanding
Answer these without looking back at the article.
- What is the difference between REQUIRES_NEW and NESTED propagation in terms of how a rollback in the inner method affects the outer transaction?
- How does @Transactional behave when placed on an interface method rather than the implementation class?
- Why does Spring recommend using @Transactional on concrete classes rather than interfaces when using CGLIB proxies?
- How does `spring.jpa.open-in-view` interact with @Transactional boundaries?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.