Why @Transactional May Not Work: Common Failure Scenarios and Source‑Code Analysis

This article explains three typical situations in which Spring's @Transactional annotation becomes ineffective—non‑public methods, internal self‑calls, and caught exceptions—illustrates each case with runnable code examples, and dives into the underlying AOP and transaction‑management source code to show why the proxy logic is bypassed.

Top Architect
Top Architect
Top Architect
Why @Transactional May Not Work: Common Failure Scenarios and Source‑Code Analysis

@Transactional Annotation Failure Scenarios

Spring's @Transactional support is implemented through dynamic proxies; when certain conditions are not satisfied the proxy is not applied and the transaction does not start, causing database operations to be committed even when an exception occurs.

Scenario 1 – Non‑public method

If a method marked with @Transactional is declared with a visibility other than public, Spring ignores the annotation because the proxy only intercepts public methods.

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

    @Transactional
    void insertTestWrongModifier() { // default (package‑private) visibility
        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 results in no transaction, so the insert before the exception is not rolled back.

Scenario 2 – Internal self‑call

When a @Transactional method is invoked from another method within the same class, the call bypasses the proxy (it uses this), so the transaction is never started.

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

    @Transactional
    public void insertTestInnerInvoke() {
        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 – no proxy involved
        insertTestInnerInvoke();
    }
}

Running a test that calls testInnerInvoke() shows that the insert is not rolled back because the transaction never began.

Scenario 3 – Swallowing exceptions

If a @Transactional method catches the exception and does not re‑throw it, the transaction manager sees a successful execution and commits the changes.

/**
 * @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 new NeedToInterceptException("need intercept");
            }
            testMapper.insert(new Test(210,20,30));
        } catch (Exception e) {
            System.out.println("i catch exception"); // exception swallowed
        }
    }
}

The test confirms that even though an exception is thrown, the transaction is committed because the exception never propagates out of the method.

Underlying Spring Mechanism

Spring creates a BeanFactoryTransactionAttributeSourceAdvisor that scans bean methods for @Transactional. The

AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

method returns null for non‑public methods, preventing proxy creation.

protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
    // Do not allow non‑public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
    }
    // ... other logic omitted ...
}

When the proxy intercepts a method, TransactionAspectSupport#invokeWithinTransaction obtains the transaction attribute, starts a transaction, invokes the target method, and either commits or rolls back based on whether an exception propagates.

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(); // non‑transactional
    }
    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);
    }
}

Therefore, to make @Transactional work reliably you must ensure the method is public, invoke it through a Spring‑managed bean (or self‑inject the bean), and let exceptions propagate out of the transactional method.

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.

JavaaopspringSpringBoottransactionaltransaction-management
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.