One of the most common sources of bugs in Spring + Hibernate applications is misunderstanding transaction rollback behavior.
Many developers assume:
“If an exception happens, the transaction will rollback.”
That assumption is wrong by default.
Let’s clarify how it really works — and how to avoid costly data inconsistencies.
In Spring-managed transactions (@Transactional):
(RuntimeException, Error)
➡ Trigger rollback automatically
(Exception, IOException, SQLException)
➡ DO NOT trigger rollback by default
This rule surprises many developers — especially those new to Spring.
This behavior is intentional and based on Java’s exception philosophy:
Checked exceptions
→ Often represent recoverable or business-related conditions
→ Spring assumes you might want to commit and handle them
Unchecked exceptions
→ Represent programming errors or system failures
→ Spring assumes the transaction is unsafe to commit
@Transactional public void createUserWithCheckedException() throws IOException { userRepository.save(new User("Alice")); throw new IOException("Checked exception"); }
✅ The user WILL be saved
Now compare that with:
@Transactional public void createUserWithRuntimeException() { userRepository.save(new User("Bob")); throw new RuntimeException("Runtime exception"); }
❌ The transaction WILL rollback
Same code. Different exception type. Very different outcome.
You can explicitly tell Spring to rollback:
@Transactional(rollbackFor = IOException.class) public void createUserSafely() throws IOException { userRepository.save(new User("Charlie")); throw new IOException("Now it rolls back"); }
This is essential when checked exceptions should indicate transaction failure.
Sometimes you want the opposite:
@Transactional(noRollbackFor = IllegalStateException.class) public void processNonCriticalFailure() { repository.save(new Entity()); throw new IllegalStateException("Non-critical issue"); }
⚠ Use this carefully — it can be dangerous if misused.
Most mature Spring applications follow this rule:
All exceptions that should rollback a transaction should be unchecked
Example:
public class InsufficientBalanceException extends RuntimeException { public InsufficientBalanceException(String message) { super(message); } }
Why this works well:
Clear intent
Automatic rollback
Cleaner service layer
No rollbackFor noise everywhere
❌ Catching exceptions inside a @Transactional method
@Transactional public void badExample() { try { repository.save(new Entity()); throw new RuntimeException(); } catch (Exception e) { // swallowed } }
✅ Transaction will commit
❌ Data may be inconsistent
If you catch an exception, Spring assumes everything is fine unless you manually mark rollback.
Pure Hibernate (Session API)
→ Any exception usually marks the transaction rollback
Spring + Hibernate (most projects)
→ Spring rules apply (unchecked only by default)
Understanding this difference matters when debugging legacy code.
✅ Unchecked exceptions → rollback (default)
⚠ Checked exceptions → commit (default)
🔧 Use rollbackFor only when necessary
🧠 Prefer unchecked exceptions for transactional failures
❌ Don’t swallow exceptions inside transactions
If you’ve ever seen “ghost data”, partial updates, or unexpected commits — this rule is often the reason.
Understanding it once can save hours of debugging later.
💬 Have you been bitten by this behavior before?