What is rollback in transactions?
Like an "Undo" button (Ctrl+Z) in a text editor — everything you did since the last save is reverted.
🟢 Junior Level
Rollback is an operation that cancels all changes of the current transaction and returns the database to the state before it started.
Mechanism: The DBMS uses undo log (journal of old values). On rollback, each modified row is restored from undo log. In Spring, TransactionInterceptor calls Connection.rollback(), which signals the DBMS to roll back the current transaction.
Simple example
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
UPDATE accounts SET balance = balance + 10 WHERE id = 2;
-- Something went wrong!
ROLLBACK; -- Both changes cancelled, balances returned to original
Analogy
Like an “Undo” button (Ctrl+Z) in a text editor — everything you did since the last save is reverted.
Why it’s needed
Rollback guarantees atomicity (the A in ACID): either all changes are saved, or none at all.
Rollback in Spring
Spring automatically rolls back when a @Transactional method throws an exception.
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
Account fromAcc = repo.findById(from);
Account toAcc = repo.findById(to);
fromAcc.setBalance(fromAcc.getBalance().subtract(amount));
toAcc.setBalance(toAcc.getBalance().add(amount));
repo.save(fromAcc);
repo.save(toAcc);
// If RuntimeException fires here — everything automatically rolls back
}
By default
Spring rolls back only on RuntimeException and Error. Checked Exceptions (Exception subclasses) do NOT trigger rollback — Spring will COMMIT.
Why Spring decided this: RuntimeException = programming error (NPE, IAE), from which recovery is impossible → rollback. Checked Exception = expected business situation (user not found), which the caller can handle → commit.
When NOT to do rollback
- Irreversible side effects — email already sent, payment already went through external API. DB rollback won’t cancel external action.
- Long transactions with partial success — better to record partial result and handle later than lose everything.
- Logging/auditing — this data should survive even if the main transaction rolls back (use REQUIRES_NEW).
🟡 Middle Level
How Rollback works at the DB level
- All changes written to Undo Log
- On ROLLBACK, DBMS reads Undo Log in reverse order
- Compensating actions executed, restoring old values
- Locks released only after rollback completes
Rollback in Spring Framework
Automatic (declarative)
Happens when an exception is thrown from a @Transactional method.
Programmatic (manual)
@Transactional
public void someMethod() {
try {
// logic
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
}
}
Rollback-Only State
If in a REQUIRED → REQUIRED call chain one method throws RuntimeException, Spring marks the transaction with a rollback-only flag.
Even if the outer method catches the exception and tries to complete successfully — on exit Spring sees the flag and throws UnexpectedRollbackException.
Partial Rollback (Savepoints)
Through Propagation.NESTED, you can roll back only part of a transaction to a savepoint.
When NOT to use rollback
| Situation | Why | Alternative |
|---|---|---|
| Compensating transaction (Saga) | Data needed for reverse operation | Explicit compensating operation |
| Audit logging | Log must survive even on error | REQUIRES_NEW for logs |
| Partial success in batch | One failed item ≠ rollback entire batch | Handle error, continue |
Common mistakes
| Mistake | What happens | How to fix |
|---|---|---|
| Catching and swallowing exception | Spring doesn’t see exception → COMMIT | Rethrow or setRollbackOnly() |
@Transactional without rollbackFor on checked exception |
COMMIT instead of ROLLBACK | rollbackFor = Exception.class |
| Expecting full rollback in REQUIRED chain | Inner method already set rollback-only |
Use REQUIRES_NEW for independent operations |
| Rollback and subsequent EntityManager use | Session in inconsistent state | Don’t reuse EntityManager after rollback |
Comparison: automatic vs manual rollback
| Approach | Pros | Cons | When to use |
|---|---|---|---|
Automatic (@Transactional) |
Clean code, minimal boilerplate | Only on exception exit | Standard scenarios |
Manual (setRollbackOnly) |
Conditional rollback, no throw | Boilerplate, coupling to Spring API | Business logic with condition |
🔴 Senior Level
Rollback Mechanics — Database Level
PostgreSQL Rollback Process
1. Transaction enters abort state
2. All tuple modifications marked as dead:
- Inserted tuples: xmin = aborted txid → invisible to all future txns
- Updated tuples: old version remains (xmax not set), new version marked aborted
- Deleted tuples: deletion is undone (xmax cleared)
3. Row-level locks released
4. Transaction state changed to ABORTED
5. XLOG record written: XACT_ABORT
Key insight: In PostgreSQL MVCC, rollback is almost entirely logical — tuple versions marked invalid via transaction status. Physical cleanup happens later via VACUUM.
InnoDB Rollback Process
1. Transaction enters rollback state
2. Undo log records read in reverse order
3. For each undo record:
- Restore old value to data page
- Release associated locks
- Update undo log pointer
4. Transaction state → ROLLED BACK
5. Trx sys history updated
InnoDB rollback is more physical — data pages actually restored to previous state via undo log.
Spring Rollback Decision Tree
// Simplified AbstractPlatformTransactionManager
private void processRollback(DefaultTransactionStatus status, Throwable ex) {
// 1. Check if rollback is actually needed
if (ex != null && !status.getTransactionAttribute().rollbackOn(ex)) {
return; // Don't rollback — will commit instead!
}
// 2. Check if this is a nested transaction (savepoint)
if (status.hasSavepoint()) {
status.rollbackToHeldSavepoint(); // Rollback to savepoint only
return;
}
// 3. Check if this is a new transaction
if (status.isNewTransaction()) {
status.getTransactionManager().doRollback(status);
}
// 4. Participating in existing transaction
else {
status.setRollbackOnly(); // Can't actually rollback, just mark
}
}
UnexpectedRollbackException — Full Analysis
@Transactional // REQUIRED — starts Tx1
public void outer() {
try {
inner(); // REQUIRED — joins Tx1
} catch (RuntimeException e) {
// Exception caught, but Tx1 already marked rollback-only
}
// Spring tries to commit Tx1 → sees rollback-only flag
// Throws UnexpectedRollbackException
}
@Transactional // REQUIRED — joins Tx1
public void inner() {
repo.save(entity);
throw new RuntimeException(); // TransactionInterceptor → setRollbackOnly()
}
Flow:
outer()starts physical transaction Tx1inner()joins Tx1 (same connection)inner()throws RuntimeExceptionTransactionInterceptorcatches, callssetRollbackOnly()on Tx1outer()catches exception, returns normallyTransactionInterceptortries to COMMIT Tx1TransactionManagersees rollback-only flag- Throws
UnexpectedRollbackException
Solutions:
// Option 1: Don't catch — let it propagate
@Transactional
public void outer() {
inner(); // If inner fails — outer rolls back too
}
// Option 2: REQUIRES_NEW for independent operations
@Transactional
public void outer() {
try {
independentService.doWork(); // REQUIRES_NEW — separate transaction
} catch (Exception e) {
// Safe — independent tx already handled
}
}
// Option 3: Catch and rethrow
@Transactional
public void outer() {
try {
inner();
} catch (RuntimeException e) {
throw new BusinessException("Failed", e); // Still causes rollback
}
}
Edge Cases (minimum 3)
-
Rollback and Sequence:
SELECT nextval('seq')is NOT rolled back. Sequence advances independently of rollback. After rollback, sequence value already incremented — next call returns next value. This leads to gaps in sequence. -
Rollback and Hibernate
@Version: Entity version is NOT restored on rollback. Entity considered stale — on subsequent operationsOptimisticLockException. -
Rollback and L1 Cache: After rollback, Hibernate Persistence Context remains with dirty entities. Entity still in L1 cache, but state doesn’t match DB. Spring creates new EntityManager for next transaction, but if you manually reuse — problem.
-
Partial flush before rollback: Hibernate may have flushed some changes before exception occurred. At DB level they will be rolled back, but in Hibernate session they remain as persisted.
Performance Numbers: Rollback vs Commit
| Operation | PostgreSQL | InnoDB |
|---|---|---|
| Rollback latency | O(1) — transaction status changes | O(N) — undo each change |
| Commit latency | fsync WAL (~1-5 ms) | fsync redo log (~1-5 ms) |
| Lock release | Fast (in-memory) | Fast (in-memory) |
| Connection cleanup | ~10-50 μs | ~10-50 μs |
Rollback in PostgreSQL is faster than commit (no fsync needed). In InnoDB — depends on number of changes.
Memory Implications
- Undo log: Grows proportionally to transaction changes. Long transaction with large rollback = large undo log.
- Hibernate L1 cache: After rollback, entities remain in cache — memory not freed until EntityManager closed.
- Connection pool: Connection returns to pool only after rollback completes. Long rollback = connection occupied longer.
Thread Safety
Rollback always happens in the same thread that executed the transaction. ThreadLocal state (TransactionSynchronizationManager) guarantees isolation. @Async methods have separate transaction context — their rollback doesn’t affect caller.
Production War Story
Payment processing service: outer() called inner() with REQUIRED. inner() failed with DataIntegrityViolationException. outer() caught Exception, logged, and continued — expecting the transaction to be alive. Instead, Spring threw UnexpectedRollbackException on commit. All payments failed with 500. Fix: inner() moved to REQUIRES_NEW — its rollback doesn’t affect outer transaction.
Monitoring
Debug logging:
logging:
level:
org.springframework.transaction: DEBUG
org.hibernate.SQL: DEBUG
Micrometer metrics:
@Bean
public TransactionSynchronization transactionMetrics(MeterRegistry registry) {
return new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
registry.counter("transaction.rollback").increment();
}
}
};
}
Actuator:
GET /actuator/metrics/transaction.rollback
GET /actuator/metrics/transaction.commit
Highload Best Practices
- Minimal transaction duration: Longer transaction = larger undo log and higher rollback cost.
- Batch processing with savepoints: Split large batch operations into chunks. On one chunk error — rollback only that chunk.
- Avoid catch-and-swallow: If you caught Exception — explicitly call
setRollbackOnly()or rethrow. - Use
REQUIRES_NEWfor side-effects: Logging, notifications — shouldn’t block main transaction on rollback. - Monitor rollback rate: High % rollback = problem in business logic or validation, not in transactions.
- Test rollback behavior: Integration tests should verify data actually rolled back, not just that exception was thrown.
🎯 Interview Cheat Sheet
Must know:
- Rollback = cancel all transaction changes, return to state before BEGIN (via undo log)
- Spring automatically rolls back on RuntimeException and Error, checked exceptions → commit
- Rollback-only flag: inner REQUIRED method threw RuntimeException → entire transaction marked for rollback
- UnexpectedRollbackException: outer tries to commit, but transaction already marked rollback-only
- Savepoints (Propagation.NESTED) allow partial rollback without rolling back entire transaction
- Sequence values (nextval) are NOT rolled back — leads to gaps in sequence
Common follow-up questions:
- What happens if you catch exception in @Transactional method? — Spring doesn’t see exception → commit, need setRollbackOnly()
- How does rollback differ in PostgreSQL vs InnoDB? — PG: logical (tx status), InnoDB: physical (undo log reverse)
- When is rollback undesirable? — Irreversible side effects (email, external API), partial success in batch
- How to implement conditional rollback? — TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
Red flags (DO NOT say):
- “Rollback rolls back sequence” — nextval is non-transactional, gaps are inevitable
- “Caught exception = rollback” — Spring doesn’t see caught exceptions
- “EntityManager can be reused after rollback” — Session in inconsistent state
Related topics:
- [[19. Which exceptions cause rollback by default]]
- [[20. How to configure rollback for checked exceptions]]
- [[14. What does Propagation.NESTED do]]
- [[1. Decode each letter of ACID]]