Why Does @Transactional Fail? Common Pitfalls and Fixes

This article explains the main reasons why Spring's @Transactional annotation may become ineffective—including non‑public method modifiers, internal method calls, and swallowed exceptions—provides code examples, test cases, and a deep dive into the underlying proxy and AOP mechanisms.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Why Does @Transactional Fail? Common Pitfalls and Fixes

Transactional Failure Scenarios

First case: When a method annotated with @Transactional has a non‑public modifier, the annotation will not take effect. Example code:

/**
 * @author zhoujy
 */
@Component
public class TestServiceImpl {
    @Resource
    TestMapper testMapper;

    @Transactional
    void insertTestWrongModifier() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }
}

Calling this method from another bean in the same package:

@Component
public class InvokcationService {
    @Resource
    private TestServiceImpl testService;

    public void invokeInsertTestWrongModifier() {
        // call the non‑public @Transactional method
        testService.insertTestWrongModifier();
    }
}

Test case:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
    @Resource
    InvokcationService invokcationService;

    @Test
    public void testInvoke() {
        invokcationService.invokeInsertTestWrongModifier();
    }
}

Because the method is not public, the transaction is not started, so the insert operation is not rolled back. Making the method public restores normal transaction behavior.

Second Scenario

Calling a @Transactional method from within the same class also prevents the transaction from starting. Example:

/**
 * @author zhoujy
 */
@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Transactional
    public void insertTestInnerInvoke() {
        // normal public transactional method
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }

    public void testInnerInvoke() {
        // internal call to transactional method
        insertTestInnerInvoke();
    }
}

Test case demonstrates that the external call works (transaction opens), while the internal call does not (transaction stays inactive).

Third Scenario

If a transactional method catches an exception without re‑throwing it, the transaction will not roll back:

/**
 * @author zhoujy
 */
@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Transactional
    public void insertTestCatchException() {
        try {
            int re = testMapper.insert(new Test(10,20,30));
            if (re > 0) {
                // throw during execution
                throw new NeedToInterceptException("need intercept");
            }
            testMapper.insert(new Test(210,20,30));
        } catch (Exception e) {
            System.out.println("i catch exception");
        }
    }
}

Test case shows that although an exception is thrown, it is caught inside the method, so the insert of the second record is not rolled back.

Analysis of Why @Transactional Does Not Work

First Reason

The @Transactional annotation relies on dynamic proxies. If a method is not public, Spring’s transaction attribute source returns null, so no proxy is created for that method.

Spring checks each bean with BeanFactoryTransactionAttributeSourceAdvisor. It scans methods for @Transactional metadata; non‑public methods are ignored, preventing proxy creation.

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
    if (!pc.getClassFilter().matches(targetClass)) {
        return false;
    }
    MethodMatcher methodMatcher = pc.getMethodMatcher();
    if (methodMatcher == MethodMatcher.TRUE) {
        return true;
    }
    // iterate over class methods
    Set<Class<?>> classes = new LinkedHashSet<>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
    classes.add(targetClass);
    for (Class<?> clazz : classes) {
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
        for (Method method : methods) {
            if (methodMatcher.matches(method, targetClass)) {
                return true;
            }
        }
    }
    return false;
}

If all methods are non‑public, Spring does not create a proxy at all.

When a class contains only non‑public methods, the resulting bean is not a proxy.

Proxy Not Invoked

If a class has both public and non‑public @Transactional methods, a proxy is created because of the public method, but the non‑public method still does not trigger transaction logic.

@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Transactional
    public void insertTest() {
        // transactional logic
    }

    @Transactional
    void insertTestWrongModifier() {
        // this will not start a transaction
    }
}

The proxy intercepts calls to the public method but not to the package‑private one.

Second Reason

Internal calls to @Transactional methods bypass the proxy because they use this instead of the proxy instance.

Since transaction management is based on dynamic proxies, an internal call does not go through the proxy, so the transaction logic is skipped.

A workaround is to inject the bean into itself and call the method via the injected proxy:

@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;
    @Resource
    TestServiceImpl self;

    @Transactional
    public void insertTestInnerInvoke() {
        // transactional work
    }

    public void testInnerInvoke() {
        // internal call via proxy
        self.insertTestInnerInvoke();
    }
}

Third Reason

If a transactional method catches its own exception and does not re‑throw it, Spring never sees the exception, so the transaction is not rolled back.

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
    final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    if (txAttr == null) {
        return invocation.proceedWithInvocation();
    }
    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, methodIdentification(method, targetClass));
    try {
        Object retVal = invocation.proceedWithInvocation();
        commitTransactionAfterReturning(txInfo);
        return retVal;
    } catch (Throwable ex) {
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    } finally {
        cleanupTransactionInfo(txInfo);
    }
}

Because the catch block in the method consumes the exception, the above logic never reaches the completeTransactionAfterThrowing step, and the transaction remains committed.

In summary, @Transactional may fail due to non‑public method modifiers, internal method calls that bypass the proxy, or exceptions that are caught and not re‑thrown.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaProxyaopException Handlingspringtransactional
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.