Why Many Large Companies Discourage Using @Transactional in Spring

The article explains common pitfalls that cause Spring @Transactional to fail or not roll back, such as incorrect method visibility, final or static modifiers, internal method calls, beans not managed by Spring, multithreading, unsupported database engines, misconfigured propagation, swallowed exceptions, and improper rollback settings, and offers practical solutions for each case.

Java Companion
Java Companion
Java Companion
Why Many Large Companies Discourage Using @Transactional in Spring

1. Transaction Not Effective

Spring creates a proxy for each bean that participates in AOP. Only public methods can be intercepted. If a method is private, protected, package‑private, or final, the proxy cannot apply the @Transactional interceptor, so no transaction is started.

@Service
public class UserService {
    @Transactional
    private void add(UserModel userModel) { // not intercepted
        saveData(userModel);
        updateData(userModel);
    }
}

Static methods suffer the same limitation because they cannot be proxied.

Static methods cannot be wrapped by Spring AOP and therefore cannot participate in declarative transactions.

Internal method calls bypass the proxy. When a method in the same class invokes another @Transactional method via this, the call is not routed through the proxy and the inner method runs without a transaction.

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

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

Work‑arounds:

Extract the transactional logic into a separate @Service bean and invoke it.

Inject the bean into itself and call the injected proxy.

Use AopContext.currentProxy() to obtain the proxy and invoke the method.

If a class is not annotated with @Component, @Service, @Controller, etc., Spring does not create a bean, so no proxy and no transaction are applied.

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

Spring binds a database connection to the current thread via a ThreadLocal. When a transactional method spawns a new thread, the child thread receives a different connection, resulting in two independent transactions. An exception in the child thread does not cause a rollback of the parent transaction.

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

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

MySQL tables that use the MyISAM engine do not support transactions; operations on such tables are never rolled back even if the surrounding method is annotated.

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

In a plain Spring (non‑Boot) project you must configure a transaction manager and an AOP pointcut. Forgetting this configuration disables transaction handling.

2. Transaction Not Rolling Back

2.1 Wrong propagation

Only propagation values that create a transaction ( REQUIRED, REQUIRES_NEW, NESTED) start a new transaction. Using Propagation.NEVER or other non‑creating types prevents a transaction from being started.

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

2.2 Swallowing exceptions

If a transactional method catches an exception and does not re‑throw it, Spring treats the method as successful and will not roll back.

@Transactional
public void add(UserModel userModel) {
    try {
        saveData(userModel);
        updateData(userModel);
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
}

2.3 Throwing non‑runtime exceptions

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

@Transactional
public void add(UserModel userModel) throws Exception {
    try {
        saveData(userModel);
        updateData(userModel);
    } catch (Exception e) {
        log.error(e.getMessage(), e);
        throw new Exception(e);
    }
}

2.4 Misconfigured rollbackFor

If rollbackFor lists an exception type that is never thrown, the transaction will not roll back for the actual exception (e.g., SqlException).

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

To guarantee rollback for any error, set rollbackFor = Exception.class or Throwable.class.

2.5 Nested transaction rollback scope

When an inner method annotated with propagation = Propagation.NESTED throws an exception, the outer transaction also rolls back because the exception propagates to the outer proxy.

@Transactional
public void add(UserModel userModel) throws Exception {
    userMapper.insertUser(userModel);
    roleService.doOtherThing(); // NESTED
}

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

To roll back only the inner transaction, catch the exception inside the outer method and prevent it from bubbling up.

3. Other Considerations

3.1 Large‑transaction problem

Annotating an entire service method with @Transactional may unintentionally include many read‑only queries, leading to long‑running transactions. Extract the truly transactional statements into a separate method or service to keep the transaction scope small.

3.2 Programmatic (declarative vs. programmatic) transactions

Spring provides TransactionTemplate for programmatic transaction management, which avoids proxy‑related pitfalls and gives finer‑grained control.

@Autowired
private TransactionTemplate transactionTemplate;

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

While @Transactional is convenient for simple cases, TransactionTemplate is preferable for complex scenarios where proxy limitations might cause transaction loss.

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.

BackendJavatransactionAOPspringMySQLmyisam
Java Companion
Written by

Java Companion

A highly professional Java public account

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.