Backend Development 14 min read

Understanding Spring Transaction Propagation and Common Pitfalls

This article explains Spring's unified transaction model, the @Transactional annotation, and the various propagation and isolation settings, then details ten typical scenarios—such as missing Spring management, final or private methods, internal calls, wrong propagation, multithreading, and unsupported databases—that cause transactions to fail, providing code examples and solutions to avoid each issue.

Top Architect
Top Architect
Top Architect
Understanding Spring Transaction Propagation and Common Pitfalls

Spring provides a unified programming model for transaction APIs such as JTA, JDBC, Hibernate, and JPA, and its declarative transaction support allows enabling transactions simply by annotating methods with @Transactional .

However, if a transaction is not configured correctly, it may become ineffective, leading to data inconsistency and costly manual fixes. This article shares the correct usage of Spring transactions and common failure scenarios.

Transaction Propagation Types

// If a transaction exists, join it; otherwise create a new one (default)
@Transactional(propagation=Propagation.REQUIRED)
// Do not start a transaction for this method
@Transactional(propagation=Propagation.NOT_SUPPORTED)
// Always create a new transaction, suspending the existing one
@Transactional(propagation=Propagation.REQUIRES_NEW)
// Must run within an existing transaction, otherwise throw an exception
@Transactional(propagation=Propagation.MANDATORY)
// Must NOT run within a transaction, otherwise throw an exception
@Transactional(propagation=Propagation.NEVER)
// If a transaction exists, join it; otherwise run without a transaction
@Transactional(propagation=Propagation.SUPPORTS)

Isolation Levels

// READ_UNCOMMITTED: can read uncommitted data (dirty reads)
@Transactional(isolation=Isolation.READ_UNCOMMITTED)
// READ_COMMITTED: can read committed data (default for Oracle)
@Transactional(isolation=Isolation.READ_COMMITTED)
// REPEATABLE_READ: can repeat reads (default for MySQL)
@Transactional(isolation=Isolation.REPEATABLE_READ)
// SERIALIZABLE: full isolation
@Transactional(isolation=Isolation.SERIALIZABLE)

@Transactional Annotation Attributes

The annotation can be placed on interfaces, classes, or methods, and its parameters control which exceptions trigger rollback.

Common Scenarios Where Spring Transactions Fail

1. Transactional method not managed by Spring

public class ProductServiceImpl extends ServiceImpl
implements IProductService {
    @Autowired
    private ProductMapper productMapper;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateProductStockById(Integer stockCount, Long productId) {
        productMapper.updateProductStockById(stockCount, productId);
    }
}

If the class is not annotated with @Service , it is not registered in the Spring IoC container, so the @Transactional annotation has no effect.

2. Method declared as final

@Service
public class OrderServiceImpl {
    @Transactional
    public final void cancel(OrderDTO orderDTO) {
        // cancel order logic
    }
}

Spring creates proxies via JDK dynamic proxy or CGLIB; a final method cannot be overridden, so the proxy cannot add transaction logic.

3. Non- public method

@Service
public class ProductServiceImpl {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void updateProductStockById(Integer stockCount, String productId) {
        productMapper.updateProductStockById(stockCount, productId);
    }
}

Only public methods are eligible for proxy‑based transaction interception.

4. Self‑invocation within the same class

@Service
public class OrderServiceImpl {
    @Autowired
    private ProductMapper productMapper;

    public ResponseEntity submitOrder(Order order) {
        // generate order number and insert
        this.updateProductStockById(order.getProductId(), 1L);
        return new ResponseEntity(HttpStatus.OK);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateProductStockById(Integer num, Long productId) {
        productMapper.updateProductStockById(num, productId);
    }
}

Because submitOrder calls updateProductStockById directly, the call bypasses the proxy, so the transaction on the latter is not applied.

5. Propagation type that does not support a transaction

@Service
public class OrderServiceImpl {
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateProductStockById(Integer num, Long productId) {
        productMapper.updateProductStockById(num, productId);
    }
}

Using NOT_SUPPORTED disables transaction support for the method.

6. Exception swallowed inside the method

@Service
public class OrderServiceImpl {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateProductStockById(Integer num, Long productId) {
        try {
            productMapper.updateProductStockById(num, productId);
        } catch (Exception e) {
            log.error("Error updating product Stock: {}", e);
        }
    }
}

Catching and ignoring exceptions prevents the transaction manager from seeing the failure, so rollback will not occur.

7. Database engine does not support transactions

Using MySQL's MyISAM storage engine, which lacks transaction support, makes any Spring transaction ineffective.

8. Transaction manager not configured

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

Without a transaction manager bean (or equivalent XML configuration), Spring cannot manage transactions.

9. Wrong propagation setting (e.g., Propagation.NEVER )

@Transactional(propagation = Propagation.NEVER)
public void cancelOrder(UserModel userModel) {
    // cancel logic
}

NEVER forbids any surrounding transaction, causing an exception if one exists.

10. Multithreaded invocation

@Service
public class OrderServiceImpl {
    @Transactional
    public void orderCommit(OrderModel orderModel) throws Exception {
        orderMapper.insertOrder(orderModel);
        new Thread(() -> {
            messageService.sendSms();
        }).start();
    }
}

@Service
public class MessageService {
    @Transactional
    public void sendSms() {
        // send sms logic
    }
}

Transactions are bound to the thread via ThreadLocal; spawning a new thread breaks this binding, so the inner transaction cannot roll back the outer one.

Summary

The article introduces Spring transaction propagation attributes, demonstrates how to use @Transactional , and enumerates ten typical situations where transactions become ineffective. Understanding these pitfalls helps developers avoid accidental data inconsistency in Spring‑based backend systems.

backendJavatransactionSpringSpring BootPropagationJPA
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

login 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.