Skip to content
thesarfo

Recipe

Spring Fundamentals: Transactions

Wiring up @Transactional with JdbcTemplate, why rollback only applies to runtime exceptions by default, and using noRollbackFor to override that.

views 0

A minimal setup wiring a DataSource, a JdbcTemplate, and transaction management:

@Configuration
@EnableTransactionManagement // enables transaction management; you still mark which methods should be wrapped in a transaction
@ComponentScan(basePackages = {"dev.thesarfo.repository", "dev.thesarfo.service"})
public class ProjectConfig {
@Bean
public DataSource datasource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl("jdbc:mysql://localhost/tilly");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
// hides JDBC's boilerplate behind a template
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
// enables transactions
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

A repository using the template, and a service wrapping its call in a transaction:

@Repository
public class ProductRepository {
private final JdbcTemplate jdbcTemplate;
@Autowired
public ProductRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void addProduct(String name) {
String sql = "INSERT INTO product VALUES(NULL, ?)";
jdbcTemplate.update(sql, name);
}
}
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// @Transactional ensures the DB insertion doesn't take effect if an exception is thrown
@Transactional
public void addOneProduct() {
productRepository.addProduct("Beer");
throw new RuntimeException(":(");
}
}

Running addOneProduct() inserts “Beer,” then throws — but the insert never actually lands in the table, because the whole method ran inside one transaction that gets rolled back on the exception.

Important: rollback only happens automatically for runtime (unchecked) exceptions. If you throw a checked exception instead, you have to declare and propagate it everywhere the method is called, and the transaction will not roll back on it unless you say so explicitly.

Overriding the default rollback behavior

Since runtime exceptions roll back by default, you can explicitly opt a specific exception type out of that:

@Transactional(noRollbackFor = RuntimeException.class)
public void test() {
System.out.println("No rollback");
throw new RuntimeException("Will not be thrown"); // won't trigger a rollback, despite being a RuntimeException
}

And the same works in reverse — telling Spring to roll back on a checked exception it would otherwise ignore:

@Transactional(noRollbackFor = Exception.class)
public void anotherTest() throws Exception {
System.out.println("No rollback for the checked exception");
throw new Exception("Will not be thrown");
}