Understanding Spring @Transactional: Common Transaction Pitfalls and Source‑Code Analysis

This article explains why Spring transactions may fail, demonstrates simple examples with and without the @Transactional annotation, details the annotation’s definition and attributes, and provides a step‑by‑step source‑code analysis of transaction creation, commit, rollback, and cleanup in Spring 4.3.12.

Zhuanzhuan Tech
Zhuanzhuan Tech
Zhuanzhuan Tech
Understanding Spring @Transactional: Common Transaction Pitfalls and Source‑Code Analysis

1. Small Transaction Examples

1.1 No @Transactional, Exception Not Rolled Back

Before executing the method, the database contains a record. The following code inserts a record and then throws a RuntimeException without any transaction annotation.

@Component
public class TransactionalTest {
    @Resource
    BasicPriceUploadRecordMapper basicPriceUploadRecordMapper;
    public void onAddTransactionToException() {
        BasicPriceUploadRecord base = new BasicPriceUploadRecord();
        base.setErrorMsg("No transaction annotation, exception thrown");
        base.setId(1824040965380245002L);
        basicPriceUploadRecordMapper.updateByPrimaryKeySelective(base);
        throw new RuntimeException();
    }
}

After execution the database still shows the inserted record, proving that the exception did not trigger a rollback.

1.2 Add @Transactional, Exception Rolled Back

Now the method is annotated with @Transactional, specifying the transaction manager and rollbackFor = Exception.

@Component
public class TransactionalTest {
    @Resource
    BasicPriceUploadRecordMapper basicPriceUploadRecordMapper;

    /**
     * Add declarative annotation, exception occurs
     */
    @Transactional(transactionManager = "valuationTransactionManager", rollbackFor = Exception.class)
    public void addTransactionToException() {
        BasicPriceUploadRecord base = new BasicPriceUploadRecord();
        base.setErrorMsg("Add @Transactional, exception thrown");
        base.setId(1824040965380245002L);
        basicPriceUploadRecordMapper.updateByPrimaryKeySelective(base);
        throw new RuntimeException();
    }
}

After execution the database shows no change, confirming that the transaction was rolled back as expected.

2. @Transactional Annotation

2.1 Definition

@Transactional is a Spring annotation for declarative transaction management. It works through AOP and can be placed on interfaces, methods, classes, or public class methods.

2.2 Common Attributes

value|transactionManager: specify the transaction manager name
propagation: transaction propagation behavior
isolation: transaction isolation level
timeout: transaction timeout
readOnly: whether the transaction is read‑only
rollbackFor: exception classes that trigger rollback
noRollbackFor: exception classes that do NOT trigger rollback

Propagation behaviors:
    REQUIRED – join existing transaction or create a new one
    SUPPORTS – join if exists, otherwise execute non‑transactionally
    MANDATORY – must join an existing transaction, else throw
    REQUIRES_NEW – always start a new transaction, suspend the current one
    NOT_SUPPORTED – execute non‑transactionally, suspend the current one
    NEVER – must not run within a transaction, else throw
    NESTED – execute within a nested transaction if a transaction exists

Isolation levels (default is DEFAULT):
    DEFAULT – use the underlying DB default
    READ_UNCOMMITTED – see uncommitted changes (dirty reads)
    READ_COMMITTED – see only committed changes (prevents dirty reads)
    REPEATABLE_READ – consistent reads within the transaction (prevents non‑repeatable reads)
    SERIALIZABLE – highest isolation, prevents phantom reads

3. Source‑Code Analysis (Spring 4.3.12)

3.1 Simple Transaction Flowchart

3.2 Proxy Class Generation

When a bean is instantiated and a @Transactional annotation is detected, Spring creates a transaction‑enhanced proxy. The creation chain is:

AbstractAutowireCapableBeanFactory.createBean => doCreateBean() => initializeBean() => applyBeanPostProcessorsAfterInitialization() => postProcessAfterInitialization() => AbstractAutoProxyCreator.postProcessAfterInitialization() => wrapIfNecessary() => createProxy() (proxyFactory.setProxyTargetClass(true))

3.3 Method Entry in Proxy Class

The entry point is TransactionInterceptor.invoke(), which eventually calls TransactionAspectSupport.invokeWithinTransaction() to wrap the target method execution in a transaction.

public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
        return this.invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
            public Object proceedWithInvocation() throws Throwable {
                return invocation.proceed();
            }
        });
    }
}

3.4 Core Transaction Logic

TransactionAspectSupport.invokeWithinTransaction()

obtains the transaction attributes and manager, then distinguishes between declarative and programmatic transactions.

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final TransactionAspectSupport.InvocationCallback invocation) throws Throwable {
    final TransactionAttribute txAttr = this.getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
    final PlatformTransactionManager tm = this.determineTransactionManager(txAttr);
    final String joinpointIdentification = this.methodIdentification(method, targetClass, txAttr);
    if (txAttr != null && tm instanceof CallbackPreferringPlatformTransactionManager) {
        // programmatic transaction handling (omitted)
    } else {
        TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
        Object retVal = null;
        try {
            retVal = invocation.proceedWithInvocation();
        } catch (Throwable ex) {
            this.completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        } finally {
            this.cleanupTransactionInfo(txInfo);
        }
        this.commitTransactionAfterReturning(txInfo);
        return retVal;
    }
}

3.4.1 Starting a Transaction

TransactionAspectSupport.createTransactionIfNecessary()

checks whether a transaction already exists and creates a new one if required.

protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
    if (txAttr != null && ((TransactionAttribute)txAttr).getName() == null) {
        txAttr = new DelegatingTransactionAttribute((TransactionAttribute)txAttr) {
            public String getName() { return joinpointIdentification; }
        };
    }
    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            status = tm.getTransaction((TransactionDefinition)txAttr);
        } else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured");
        }
    }
    return this.prepareTransactionInfo(tm, (TransactionAttribute)txAttr, joinpointIdentification, status);
}

3.4.2 Rolling Back a Transaction

TransactionAspectSupport.completeTransactionAfterThrowing()

decides whether to roll back based on the exception type and the rollback rules defined in the transaction attributes.

protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
    if (txInfo != null && txInfo.hasTransaction()) {
        if (txInfo.transactionAttribute.rollbackOn(ex)) {
            try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); }
            catch (Throwable rollbackEx) { /* omitted */ }
        } else {
            try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); }
            catch (Throwable commitEx) { /* omitted */ }
        }
    }
}

3.4.3 Committing a Transaction

TransactionAspectSupport.commitTransactionAfterReturning()

simply delegates to the transaction manager’s commit method when the transaction is still active.

protected void commitTransactionAfterReturning(TransactionInfo txInfo) {
    if (txInfo != null && txInfo.hasTransaction()) {
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}

3.4.4 Cleaning Up Transaction Information

After commit or rollback, AbstractPlatformTransactionManager.cleanupAfterCompletion() marks the transaction as completed, clears thread‑local synchronization, releases resources, and resumes any previously suspended transaction.

private void cleanupAfterCompletion(DefaultTransactionStatus status) {
    status.setCompleted();
    if (status.isNewSynchronization()) {
        TransactionSynchronizationManager.clear();
    }
    if (status.isNewTransaction()) {
        this.doCleanupAfterCompletion(status.getTransaction());
    }
    if (status.getSuspendedResources() != null) {
        this.resume(status.getTransaction(), (SuspendedResourcesHolder)status.getSuspendedResources());
    }
}

Summary of Common Transaction Failures and Solutions

Self‑invocation within the same class: The call bypasses the proxy, so @Transactional is ignored. Solution: Move the logic to another bean or inject the bean into itself.

Checked exceptions not causing rollback: By default only RuntimeException triggers rollback. Solution: Specify rollbackFor = Exception.class or throw a RuntimeException.

Multi‑threaded operations: Each thread gets its own JDBC connection; the transaction context is thread‑local, so work in other threads is not part of the transaction. Solution: Use distributed transaction frameworks or manage rollback manually.

Incorrect propagation settings: Misusing REQUIRES_NEW, NOT_SUPPORTED, etc., can suspend or start unintended transactions. Solution: Choose the appropriate propagation behavior for each method.

When a method finishes normally, Spring calls commitTransactionAfterReturning(); when an exception occurs, it calls completeTransactionAfterThrowing(). Both methods first verify the existence of a transaction and then decide whether to commit or roll back based on the defined rules.

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.

backendJavatransactionAOPdatabaseSpring
Zhuanzhuan Tech
Written by

Zhuanzhuan Tech

A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.

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.