Interview Prep Zoneby CuriouserLabs

Question 4 of 5 · senior level

When does @Transactional roll back — and what's your checklist for the classic ways it silently fails?

🎤 Say this first

Default rollback rule: unchecked exceptions (RuntimeException/Error) roll back; checked exceptions commit — a historical EJB-ism that surprises everyone. Override with rollbackFor/noRollbackFor. The silent-failure checklist: self-invocation (proxy bypassed), non-public methods, exception caught inside the method (nothing propagates to the interceptor), checked exception thrown, @Transactional on the wrong layer/class with a mismatched transaction manager, and multi-datasource setups where the wrong manager runs the show.

The full picture

Four silent failures in one class
@Service
public class TransferService {

    @Transactional
    public void transfer(...) throws SettlementException {
        debit(...);
        try {
            credit(...);
        } catch (DataAccessException e) {     // ① caught -> interceptor never sees it
            log.warn("credit failed", e);     //    method returns normally -> COMMIT (debit only!)
        }
        settle(...);                          // ② throws CHECKED SettlementException
    }                                         //    -> default rule: still COMMITS

    @Transactional                            // ③ called via this.recalc() elsewhere
    void recalc() { … }                       //    in class -> proxy bypassed
                                              // ④ non-public -> not advised at all
}

// Fixes: rethrow or mark rollbackOnly; @Transactional(rollbackFor = SettlementException.class);
// move recalc() to its own bean; make advised methods public.
  • Programmatic escape hatch when you must catch: TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() — or better, restructure so the exception propagates.
  • Layer discipline: @Transactional belongs on the service/use-case boundary — one business operation, one transaction. On repositories it fragments atomicity; on controllers it drags HTTP serialization into the TX window.
  • Keep transactions short: no HTTP calls, no heavy computation inside — you're holding a connection and locks. The connection-pool math (pool size vs concurrent TX duration) is where this becomes an outage.
  • Verify wiring in tests: an integration test that asserts rollback actually happened (row absent) catches ①–④ mechanically.

🔄 Likely follow-up questions

  • Why is holding a transaction across a remote HTTP call an outage pattern?
  • How does @Transactional interact with @Async on the same method?
  • What does the transaction interceptor do with an Error (e.g., OOM) vs Exception?