I added @Transactional on the method. Spring never started the transaction. 🤯
public void updateOrder(Long id) {
paymentService.verifyPayment();
updateOrderInTx(id);
}
@Transactional
public void updateOrderInTx(Long id) {
// DB operations
}
Code looked perfect until some database updates succeeded, others failed midway, and nothing rolled back.

Most people think @Transactional is magic. It isn’t. It’s implemented using a Spring proxy.
When another Spring bean calls your service, the call goes through that proxy. The proxy opens the transaction, commits it, or rolls it back.
Here, the method call never leaves the current object. The proxy is bypassed.
No proxy. No transaction.
One fix is to move the transactional work into another Spring bean:
OrderService → TransactionService → @Transactional → Database
Now the method call goes through the Spring proxy and the transaction starts.
There are two easy ways to misuse @Transactional:
- Keep it open while waiting for network calls.
- Expect it to work during self-invocation.
The annotation describes the transaction. The Spring proxy applies it. Once I understood that distinction, a lot of Spring’s transactional “magic” stopped being magic.