@Transactional tells Spring to wrap a method call in a database transaction via an AOP proxy — begin before the method runs, commit if it returns normally, roll back if it throws. It does not do this by editing your method; it does it from the outside, in a proxy that sits between the caller and your bean.
What happens on a call
When you call an annotated method through the proxy, an interceptor (TransactionInterceptor) runs first. It asks a PlatformTransactionManager to start (or join) a transaction according to the propagation rule, binds the resulting connection to the current thread, then invokes your method. On normal return it commits; on an unchecked exception it rolls back; then it unbinds the connection.
@Service
class AccountService {
private final JdbcTemplate jdbc;
AccountService(JdbcTemplate jdbc) { this.jdbc = jdbc; }
@Transactional
public void transfer(long from, long to, BigDecimal amount) {
jdbc.update("UPDATE account SET balance = balance - ? WHERE id = ?", amount, from);
jdbc.update("UPDATE account SET balance = balance + ? WHERE id = ?", amount, to);
// both run on the SAME thread-bound connection; commit happens after return
}
}
Where to put it
On the service layer — public methods of a Spring bean. That layer is the natural transaction boundary: one business operation, one transaction. Concretely:
- It must be a Spring-managed bean, so the proxy exists.
- The method should be public. With the default Spring AOP proxies,
@Transactionalonprivate/protected/package-private methods is silently ignored. - Don't put it on repositories alone (each call becomes its own tiny transaction, defeating atomicity across calls) or on controllers (you'd hold the transaction open across view rendering / serialization).
- Calls must arrive through the proxy — external callers, or a self-injected proxy reference. A plain
this.method()bypasses it (the self-invocation trap).