Using Transactions for Read-Only Operations in Spring Boot
Last Updated :
23 Jul, 2025
In database-driven applications, transactions are essential for maintaining data integrity and consistency. While transactions are typically associated with operations that modify data, they can also be effectively used for read-only operations. Using transactions for read-only operations ensures that data remains consistent during read operations, even in highly concurrent environments. In this article, we will explore how to use transactions for read-only operations in Spring Boot.
Using transactions for read-only operations can help optimize performance and ensure data consistency. In a Spring application, we can annotate service layer methods with @Transactional(readOnly = true) to indicate that the method is read-only. This hints to the underlying persistence framework (e.g., Hibernate) to optimize the transaction by avoiding unnecessary locks and flushing operations.
Key Points:
- Transactional Read-Only: By annotating the method with
@Transactional(readOnly = true), the transaction is optimized for read operations. - Consistency: Ensures that data remains consistent during the transaction, even if other operations are modifying the data concurrently.
- Performance: Read-only transactions can improve performance by minimizing the overhead of transaction management.
Implementation of Using Transactions for Read-Only Operations in Spring Boot
Step 1: Create the Spring Boot Project
Create a new Spring Boot project using IntelliJ IDEA with the following options:
- Name:
spring-boot-readonly-transaction - Language: Java
- Type: Maven
- Packaging: Jar
Click on the Next button.
Step 2: Add the Dependencies
Add the following dependencies into the Spring Boot project.
Project Structure
After creating the project, the file structure should look like this:
This is the basic structure of a Spring Boot project, with the main Java files under src/main/java and resource files under src/main/resources.
Step 3: Configure Application Properties
Open the application.properties file and add the following MySQL and Hibernate configuration:
spring.application.name=spring-boot-readonly-transaction
spring.datasource.url=jdbc:mysql://localhost:3306/testdb
spring.datasource.username=root
spring.datasource.password=mypassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
This file configures the MySQL database connection and enables Hibernate to manage the database schema.
Step 4: Create the data.sql
This file contains the initial data for the Product entity and will be automatically loaded at startup.
INSERT INTO product (name, price) VALUES ('Product 1', 100.0);
INSERT INTO product (name, price) VALUES ('Product 2', 150.0);
INSERT INTO product (name, price) VALUES ('Product 3', 200.0);The data.sql file pre-populates the database with products, which will be available for retrieval upon application startup.
Step 5: Create the Product Entity
Create the Product class representing the product table in the database.
Java
package com.gfg.springbootreadonlytransaction;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}
The Product entity maps the product table in the database and includes basic fields like id, name, and price.
Step 6: Create the ProductRepository Interface
Create the ProductRepository interface for performing CRUD operations on the Product entity.
Java
package com.gfg.springbootreadonlytransaction;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
}
The ProductRepository interface extends JpaRepository to provide built-in methods for data operations on the Product entity.
Step 7: Create the ProductService Class with a Read-Only Transaction
This service class contains a method annotated with @Transactional(readOnly = true) to indicate that it is a read-only transaction.
Java
package com.gfg.springbootreadonlytransaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional(readOnly = true)
public List<Product> getAllProducts() {
return productRepository.findAll();
}
}
The ProductService class manages the business logic, with the getAllProducts method using a read-only transaction to optimize read operations.
Step 8: Create the ProductController Class
Create the ProductController class to expose an API endpoint for fetching all the products.
Java
package com.gfg.springbootreadonlytransaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping
public List<Product> getProducts() {
return productService.getAllProducts();
}
}
The ProductController class handles HTTP requests and delegates the task of fetching products to the ProductService.
Step 9: Main Class
No changes are required in the main class. This is the main entry point for the Spring Boot application.
Java
package com.gfg.springbootreadonlytransaction;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootReadonlyTransactionApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootReadonlyTransactionApplication.class, args);
}
}
The SpringBootReadonlyTransactionApplication class is the entry point of the Spring Boot application, containing the main method.
pom.xml file:
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gfg</groupId>
<artifactId>spring-boot-readonly-transaction</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-readonly-transaction</name>
<description>spring-boot-readonly-transaction</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Step 10: Run the Application
After completing the project, it will start and run on port 8080.
Step 11: Testing the Endpoint
When you hit the /products endpoint, you should see a JSON response with the list of products stored in the database.
GET http://localhost:8080/products
This endpoint returns the list of products in JSON format, demonstrating the use of read-only transactions for fetching data.
Output:
This example project demonstrates the use of the read-only transactions in the Spring Boot application. By the setting up the project as the described and running it, we can observe the how read-only transactions are applied. It ensures the optimal performance and data consistency for the read operations.
Explore
Java Enterprise Edition
Multithreading
Concurrency
JDBC (Java Database Connectivity)
Java Frameworks
JUnit