
@Transactional was right there. Rollback still didn’t happen. 💀
The flow: user completes payment → Order, Payment, and Inventory all update in one method. If one fails, rollback everything.
Inventory update failed midway. The other two tables? Already committed. Inconsistent data sitting in prod, no rollback, no errors — just silent corruption. 💥

Why didn’t rollback happen?
We were calling the @Transactional method from another method in the same class.
@Service
public class MyService {
public void outerMethod() {
// some logic
this.innerTransactionalMethod(); // internal call
}
@Transactional
public void innerTransactionalMethod() {
// updates 3 tables
}
}
It ran like a regular method. No transaction. No rollback.

How Spring @Transactional actually works
Spring’s @Transactional works through proxies — think of it as a wrapper around your bean from the outside.
- External call → goes through the Spring proxy → transaction starts ✅
- Internal call (
this.method()) → bypasses the proxy entirely ❌
So our “transactional” method was just a plain method call with no transaction context around it. Self-invocation is the real culprit.

The fix
Move the @Transactional logic into a separate bean and call it externally:
// Orchestrator — no @Transactional
@Service
public class MyService {
@Autowired
private TxService txService;
public void outerMethod() {
txService.innerTransactionalMethod(); // external call → proxy → ✅
}
}
// Separate bean — @Transactional works correctly here
@Service
public class TxService {
@Transactional
public void innerTransactionalMethod() {
// updates 3 tables — now properly wrapped in a transaction
}
}
One small refactor. Rollback started working again. ✅

@Transactional doesn’t protect you from where you call it from. The annotation is only as good as the proxy that wraps it.