Why Your Spring @Transactional May Fail and How to Fix It

This article explains the common reasons Spring transactions become ineffective or fail to roll back—such as wrong method visibility, final modifiers, self‑invocation, non‑Spring beans, multithreading, unsupported table engines, misconfigured propagation, swallowed exceptions, and improper rollback settings—while providing practical code solutions and best‑practice recommendations.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Why Your Spring @Transactional May Fail and How to Fix It

Overview

Spring provides declarative transaction management for Java applications. The @Transactional annotation is convenient, but it works only when the underlying proxy mechanism can apply transaction semantics. Misusing visibility, method modifiers, internal calls, thread boundaries, or configuration can silently disable transactions or prevent roll‑back.

1. Scenarios Where @Transactional Is Ignored

1.1 Method visibility

Spring creates a proxy only for public methods. If a transactional method is declared private, protected or package‑private, the proxy returns null for the transaction attribute and no transaction is started.

@Service
public class UserService {
    @Transactional
    private void add(UserModel user) {
        saveData(user);
        updateData(user);
    }
}

1.2 final methods

Methods marked final cannot be overridden by the CGLIB/JDK proxy class, so the transaction interceptor cannot weave transaction logic.

@Service
public class UserService {
    @Transactional
    public final void add(UserModel user) {
        saveData(user);
        updateData(user);
    }
}

1.3 Self‑invocation (internal method calls)

Calling a transactional method from another method of the same class bypasses the proxy, therefore the transaction is not started.

@Service
public class UserService {
    public void add(UserModel user) {
        userMapper.insertUser(user);
        updateStatus(user); // direct call – no transaction
    }

    @Transactional
    public void updateStatus(UserModel user) {
        // transactional logic
    }
}

Typical solutions:

Move the transactional logic to a separate @Service bean and invoke it through the bean reference.

Inject the bean into itself (e.g., @Autowired private UserService self;) and call the method via the injected proxy.

Obtain the current proxy with AopContext.currentProxy() and invoke the method reflectively.

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    public void save(User user) {
        queryData1();
        queryData2();
        serviceB.doSave(user);
    }
}

@Service
public class ServiceB {
    @Transactional(rollbackFor = Exception.class)
    public void doSave(User user) {
        addData1();
        updateData2();
    }
}

1.4 Class not managed by Spring

If a class lacks a stereotype annotation ( @Component, @Service, @Repository, @Controller) Spring does not create a bean, and any @Transactional annotation on its methods is ignored.

//@Service
public class UserService {
    @Transactional
    public void add(UserModel user) {
        saveData(user);
        updateData(user);
    }
}

1.5 Multithreaded execution

Spring binds a transaction to the current thread via a ThreadLocal. When a new thread is started, it obtains a separate database connection, so the operations run in a different transaction and cannot be rolled back together.

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel user) throws Exception {
        userMapper.insertUser(user);
        new Thread(() -> roleService.doOtherThing()).start();
    }
}

@Service
public class RoleService {
    @Transactional
    public void doOtherThing() {
        System.out.println("save role data");
    }
}

1.6 Non‑transactional table engine

MySQL tables using the MyISAM engine do not support transactions. DML statements on such tables cannot be rolled back.

CREATE TABLE `category` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `one_category` VARCHAR(20) COLLATE utf8mb4_bin DEFAULT NULL,
    `two_category` VARCHAR(20) COLLATE utf8mb4_bin DEFAULT NULL,
    `three_category` VARCHAR(20) COLLATE utf8mb4_bin DEFAULT NULL,
    `four_category` VARCHAR(20) COLLATE utf8mb4_bin DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

1.7 Missing transaction configuration (XML)

In a classic Spring MVC project you must declare a DataSourceTransactionManager and an AOP advisor. An incorrect pointcut expression or missing tx:advice prevents transaction creation.

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<tx:advice id="advice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="pointcut" expression="execution(* com.susan.*.*(..))"/>
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>

2. Transaction Does Not Roll Back

2.1 Wrong propagation attribute

Using Propagation.NEVER disables transaction creation; if a transaction already exists an exception is thrown and the method runs without a transaction.

@Service
public class UserService {
    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel user) {
        saveData(user);
        updateData(user);
    }
}

2.2 Swallowing exceptions

If a method catches an exception and does not re‑throw it, Spring assumes the operation succeeded and commits the transaction.

@Transactional
public void add(UserModel user) {
    try {
        saveData(user);
        updateData(user);
    } catch (Exception e) {
        log.error(e.getMessage(), e);
        // no rethrow → transaction will be committed
    }
}

2.3 Checked exceptions

By default Spring rolls back only for RuntimeException and Error. Throwing a checked Exception does not trigger a rollback unless rollbackFor is configured.

@Transactional
public void add(UserModel user) throws Exception {
    try {
        saveData(user);
        updateData(user);
    } catch (Exception e) {
        log.error(e.getMessage(), e);
        throw new Exception(e); // checked exception → no rollback
    }
}

2.4 Incorrect rollbackFor

If rollbackFor lists a custom exception (e.g., BusinessException) but the actual exception thrown is SQLException or DuplicateKeyException, the transaction will not roll back.

@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel user) throws Exception {
    saveData(user);
    updateData(user);
}

2.5 Nested transaction behavior

When a method with Propagation.NESTED throws an exception, the exception propagates to the outer transaction, causing the whole transaction to roll back. To isolate the rollback, catch the exception inside the nested method.

@Transactional
public void add(UserModel user) throws Exception {
    userMapper.insertUser(user);
    try {
        roleService.doOtherThing(); // nested transaction
    } catch (Exception e) {
        log.error(e.getMessage(), e);
        // outer transaction continues
    }
}

@Service
public class RoleService {
    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("save role data");
    }
}

3. Additional Pitfalls

3.1 Large transaction scope

Annotating an entire service method with @Transactional may unintentionally include many read‑only queries, extending the transaction duration and hurting performance. Only the truly mutable operations should be wrapped in a transaction.

@Service
public class UserService {
    @Transactional
    public void add(UserModel user) throws Exception {
        // read‑only queries – should be outside the transaction
        query1();
        query2();
        query3();
        roleService.save(user); // only this needs a transaction
        update(user);
    }
}

@Service
public class RoleService {
    @Transactional
    public void save(UserModel user) throws Exception {
        saveData(user); // only this line is transactional
    }
}
Transaction diagram
Transaction diagram

3.2 Programmatic transaction management

Using TransactionTemplate gives fine‑grained control and avoids proxy‑related pitfalls. It is recommended for complex scenarios.

@Autowired
private TransactionTemplate transactionTemplate;

public void save(final User user) {
    queryData1();
    queryData2();
    transactionTemplate.execute(status -> {
        addData1();
        updateData2();
        return Boolean.TRUE;
    });
}

Advantages of programmatic transactions:

Avoids proxy‑related transaction loss.

Allows precise definition of transaction boundaries.

In summary, Spring transaction management is powerful but fragile. Understanding the proxy mechanism, method visibility, thread binding, propagation settings, and exception handling is essential to avoid silent data loss. For critical or complex flows, prefer programmatic transactions with TransactionTemplate for explicit control.

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.

JavatransactionAOPdatabasespring
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.