Why Does @Transactional(REQUIRES_NEW) Still Roll Back Your Audit Log?
When a Spring @Transactional method with REQUIRES_NEW fails to persist audit logs after a rollback, the issue often stems from misconfigured rollback rules, caught exceptions, self‑invocation, non‑public or final methods, or incorrect propagation settings, all of which prevent the new transaction from committing.
Developers often annotate a user‑registration method with @Transactional and expect an audit log written in a method annotated with @Transactional(propagation = Propagation.REQUIRES_NEW) to survive a rollback of the outer transaction. In practice the log may disappear, leading to confusion.
Ideal Flow (Normal Situation)
@Transactional
public void registerUser(User user) {
saveUser(user); // main transaction work
logService.log("User registration attempt"); // calls external bean, triggers REQUIRES_NEW
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String message) {
logMapper.insert(new Log(message));
}
}In this scenario, even if registerUser throws an exception and its transaction rolls back, the log() method runs in an independent transaction that has already been committed, so the audit record remains.
Why the Log May Still Disappear
1. rollbackFor Not Set Correctly
Spring rolls back only for RuntimeException and Error by default. If a checked exception such as IOException is thrown and rollbackFor is not specified, the transaction will not roll back.
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void log(String message) {
try {
logMapper.insert(new Log(message));
} catch (Exception e) {
throw new AuditLogException("Audit log failed", e);
}
}
}2. Exception Caught but Not Rethrown
If an exception is caught inside the REQUIRES_NEW method and not rethrown, Spring assumes the method completed successfully and will not trigger a rollback.
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void log(String message) {
try {
logMapper.insert(new Log(message));
} catch (Exception e) {
log.error("Error occurred: ", e); // caught but not rethrown → no rollback
}
}
}3. Self‑Invocation Within the Same Class
Spring creates a proxy for transactional beans. When a method in the same class calls another @Transactional method (self‑invocation), the call bypasses the proxy, so the inner transaction never starts.
@Transactional
public void registerUser(User user) {
saveUser(user);
log("User registration attempt"); // self‑call → REQUIRES_NEW ignored
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String message) {
logMapper.insert(new Log(message));
}4. @Transactional on Final or Non‑Public Methods
JDK dynamic proxies can only intercept public interface methods; CGLIB can proxy protected or package‑private methods but not final methods. Declaring a transactional method as final or non‑public prevents the proxy from applying the transaction.
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void log(String message) { // private → proxy cannot intercept
logMapper.insert(new Log(message));
}
// or
@Transactional(propagation = Propagation.REQUIRES_NEW)
public final void log(String message) { // final → CGLIB cannot proxy
logMapper.insert(new Log(message));
}
}5. Wrong Propagation Configuration
If the propagation attribute is mistakenly set to Propagation.REQUIRES instead of REQUIRES_NEW, the method will join the existing transaction rather than start a new one, causing the log to roll back together with the outer transaction.
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES) // should be REQUIRES_NEW
public void log(String message) {
logMapper.insert(new Log(message));
}
}Solution
To ensure the audit log persists, apply one or more of the following:
Place the REQUIRES_NEW method in a separate bean so the proxy can intercept the call.
Configure rollbackFor to include the exceptions you expect.
Rethrow caught exceptions or wrap them in a runtime exception.
Avoid self‑invocation; use ApplicationContext.getBean(...) to obtain the proxy.
Do not declare transactional methods as final or non‑public.
Verify that the propagation attribute is correctly set to REQUIRES_NEW.
By addressing these common pitfalls, the independent transaction will commit as intended, and audit logs will remain even when the outer transaction rolls back.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
