Why @Transactional Can Boost Query Performance in Spring Boot 3

This article examines the @Transactional annotation in Spring Boot, explains its proper and improper use, and presents extensive performance tests using JPA, JDBC, and EntityManager that reveal surprising cases where transactions improve query throughput.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Why @Transactional Can Boost Query Performance in Spring Boot 3

1. Introduction

@Transactional is the core annotation for declarative transaction management in Spring, simplifying database transaction handling by separating transaction logic from business code. It automatically opens a transaction before method execution and commits or rolls back based on exceptions, improving readability and maintainability.

Misusing @Transactional can negatively affect system performance, such as overly large transaction scopes, unnecessary fine‑grained transactions, and using transactions in read‑only scenarios, leading to longer connection times, lock contention, and reduced throughput.

2. Practical Cases

2.1 Environment Setup

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/batch
    username: root
    password: 123123
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimumIdle: 10
      maximumPoolSize: 10
---
spring:
  jpa:
    generateDdl: false
    hibernate:
      ddlAuto: update
    openInView: true
    show-sql: false

2.2 Entity Definition

@Entity
@Table(name = "o_user")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;
  private String name;
  private Integer age;
  private String phone;
  private String sex;
  // getters, setters
}

2.3 Repository Query Test (JPA)

Test 1 – without @Transactional:

private final UserRepository userRepository;
public User queryUser() {
  return this.userRepository.findById(4888888).orElse(null);
}

Average throughput: 9 700 ops/s.

Test 2 – with @Transactional:

@Transactional
public User queryUser() {}

Average throughput: 12 700 ops/s, unexpectedly higher.

Test 3 – read‑only transaction:

@Transactional(readOnly = true)
public User queryUser() {}

Average throughput: 9 300 ops/s, similar to no transaction.

2.4 JDBC Query Test

Test 1 – without @Transactional (using JdbcTemplate):

private final JdbcTemplate jdbcTemplate;
public User queryUser() {
  return this.jdbcTemplate.queryForObject(
    "select id, name, age, phone, sex from o_user where id = 4888888",
    (rs, rowNum) -> {
      User user = new User();
      user.setId(rs.getInt("id"));
      user.setAge(rs.getInt("age"));
      user.setName(rs.getString("name"));
      user.setPhone(rs.getString("phone"));
      user.setSex(rs.getString("sex"));
      return user;
    });
}

Average throughput: 25 000 ops/s.

Test 2 – with @Transactional:

@Transactional
public User queryUser() {}

Average throughput: 13 100 ops/s, performance drops as expected.

Test 3 – read‑only transaction:

@Transactional(readOnly = true)
public User queryUser() {}

Average throughput: 9 800 ops/s, comparable to the read‑write case.

2.5 EntityManager Query Test

Test 1 – without @Transactional:

private final EntityManager em;
public User queryUser() {
  return this.em.find(User.class, 4888888);
}

Average throughput: 24 000 ops/s.

Test 2 – with @Transactional:

@Transactional
public User queryUser() {
  return this.em.find(User.class, 4888888);
}

Average throughput: 13 000 ops/s.

Test 3 – read‑only transaction:

@Transactional(readOnly = true)
public User queryUser() {}

Average throughput: 9 800 ops/s.

2.6 Summary of Transactional Queries

Consistency: a transaction provides a snapshot of data at a single point in time, avoiding inconsistencies caused by concurrent modifications.

Performance optimization: Spring’s @Transactional(readOnly = true) allows the database to apply read‑only optimizations such as snapshot reads and reduced lock contention.

Connection reuse: multiple operations within the same transaction can share a single database connection, lowering connection overhead.

Compatibility with write operations: if a read method later becomes part of a larger write transaction, the existing @Transactional annotation integrates seamlessly.

Example of a read‑only service method that fetches user and order data in one transaction:

private final UserRepository userRepository;
private final OrderRepository orderRepository;

@Transactional(readOnly = true)
public UserInfoDto getUserInfo(Long userId) {
  User user = userRepository.findById(userId);
  List<Order> orders = orderRepository.findByUserId(userId);
  return new UserInfoDto(user, orders);
}
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.

PerformanceSpring BootJDBCTransactionaljpaEntityManager
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.