Transaction Manager Internals
The chapter that explains the single most confusing real-world @Transactional bug: "I called my own method, and the transaction just... didn't happen." This is why, exactly.
| Component | Role |
|---|---|
| @Transactional | The annotation you write — pure metadata, does nothing by itself. |
| TransactionInterceptor | An AOP advice that intercepts calls to annotated methods and wraps them with begin/commit/rollback logic. |
| PlatformTransactionManager | The interface actually responsible for beginning, committing, and rolling back — implemented differently per resource type. |
| DataSourceTransactionManager | A PlatformTransactionManager implementation for plain JDBC (no JPA/Hibernate involved). |
| JpaTransactionManager | A PlatformTransactionManager implementation that also manages the JPA EntityManager /persistence context (Chapter 12) alongside the database transaction. |
◆ Under the hood — this is the core mechanism
When Spring sees @Transactional on a bean's method, it doesn't modify your class's bytecode. Instead, at startup, it creates a proxy object — either a JDK dynamic proxy (if your bean implements an interface) or a CGLIB subclass proxy (if it doesn't) — and registers that proxy, not your original object, as the Spring bean everything else injects and calls. Every call into a @Transactional method actually first hits this proxy, which runs the TransactionInterceptor (begin the transaction), then delegates to your real method body, then runs commit or rollback logic afterward based on the outcome.
An external call goes through the proxy, which handles the transaction lifecycle before delegating to your real method — this is the entire mechanism §10.3's bug breaks.
▲ The #1 real-world @Transactional bug
A method calling another @Transactional method on the same class, via plain this.otherMethod(), bypasses the proxy entirely — this refers to the real object, not the proxy wrapping it. The call never passes through the TransactionInterceptor at all, so the inner method's @Transactional annotation is silently ignored.
@Service public class OrderService { public void placeOrder(OrderRequest request) { saveOrder(request); // plain "this" call — NOT through the proxy } @Transactional // SILENTLY IGNORED when called via this.saveOrder() above public void saveOrder(OrderRequest request) { orderRepository.save(toOrder(request)); inventoryRepository.decrementStock(request.productId()); // if decrementStock() fails, orderRepository.save() is NOT rolled back — // because there was never a real Spring-managed transaction wrapping this call at all } }
| Fix | How |
|---|---|
| Restructure into separate beans | Move saveOrder() into its own @Service, and have OrderService inject and call it — now the call genuinely crosses a bean boundary and goes through the proxy. |
| Self-inject (works, but a code smell) | Inject the bean's own proxy into itself (@Autowired private OrderService self;) and call self.saveOrder() instead of this.saveOrder(). |
| AopContext.currentProxy() | Requires exposeProxy = true on @EnableAspectJAutoProxy; explicitly fetch the current proxy and call through it — rarely the cleanest option. |
💻 Code example
@Service public class OrderService { public void placeOrder(OrderRequest request) { saveOrder(request); // plain "this" call — NOT through the proxy } @Transactional // SILENTLY IGNORED when called via this.saveOrder() above public void saveOrder(OrderRequest request) { orderRepository.save(toOrder(request)); inventoryRepository.decrementStock(request.productId()); // if decrementStock() fails, orderRepository.save() is NOT rolled back — // because there was never a real Spring-managed transaction wrapping this call at all } }
◆ Under the hood
Spring's TransactionSynchronizationManager is how framework code (and your own code) can register a callback to run at specific points in the transaction's lifecycle — most commonly, "only after this transaction successfully commits."
@Transactional public void placeOrder(OrderRequest request) { orderRepository.save(toOrder(request)); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { emailService.sendOrderConfirmation(request.customerEmail()); // only fires if the order genuinely committed } }); }
This directly solves a real problem: sending a confirmation email before the transaction commits risks confirming an order that then rolls back due to a later failure — afterCommit() guarantees the email only ever fires for genuinely, durably saved orders. Spring's @TransactionalEventListener with phase = AFTER_COMMIT is the more common, higher-level way to achieve the same thing via Spring's event system rather than this lower-level API directly.
💻 Code example
@Transactional public void placeOrder(OrderRequest request) { orderRepository.save(toOrder(request)); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { emailService.sendOrderConfirmation(request.customerEmail()); // only fires if the order genuinely committed } }); }
Want a visual for this concept?
Generate a diagram tailored to “Transaction Manager Internals” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →