7 Common Spring Boot 3 Performance Pitfalls and How to Fix Them
This article examines seven frequent performance problems when using Spring Data JPA in Spring Boot 3—such as eager loading, N+1 queries, returning entities from controllers, eager fetch misuse, oversized transactions, count‑heavy pagination, and logging entities—and provides concrete code‑level solutions like DTO projections, EntityGraph, slice pagination, and proper transaction separation.
Spring Boot 3.5.0 projects that use Spring Data JPA can suffer severe performance degradation due to common misconfigurations. The following seven patterns and their remedies are essential for efficient data access.
1. Selective Queries with DTO Projection
Repository methods that return full entity objects load all columns even when only a few fields are needed. Use DTO projections to fetch only required columns.
private final StudentRepository studentRepository;
public Student getStudent(Long id) {
return studentRepository.findById(id).orElseThrow();
}
public Page<Student> queryStudents(int cpage, int pageSize) {
Pageable pageable = PageRequest.of(cpage, pageSize);
return studentRepository.findAll(pageable);
}Replace with:
public record StudentDTO(Long id, String name) {}
@Query("select new com.pack.dto.StudentDTO(s.id, s.name) from Student s")
Page<StudentDTO> findStudents(Pageable pageable);
@Query("select new com.pack.dto.StudentDTO(s.id, s.name) from Student s where s.id = ?1")
StudentDTO findStudent(Long id);2. N+1 Query Problem
When a @OneToMany(fetch = FetchType.LAZY) association is accessed in a loop, JPA issues one query for the parent entities and an additional query for each child collection.
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getItems().size(); // triggers a separate query per order
}Resolve by loading the collection in a single query using a fetch join or @EntityGraph:
@Query("SELECT o FROM Order o JOIN FETCH o.items")
List<Order> findOrderAndItems();Or:
@EntityGraph(attributePaths = "items")
List<Order> findWithOrders();3. Returning Entities Directly from Controllers
Exposing entity objects in REST endpoints causes Jackson to trigger lazy loading, produces large JSON payloads, and makes serialization expensive.
@GetMapping("/orders")
public List<Order> orders() {
return orderRepository.findAll();
}Return DTOs instead. A convenient way is to use MapStruct‑plus for automatic conversion.
@GetMapping("/orders")
public List<OrderDTO> orders() {
return orderService.getOrders();
}Dependency:
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
</dependency>Mapper definition (annotation only, no extra class needed):
@AutoMapper(target = OrderDTO.class)
@Entity
@Table(name = "x_order")
public class Order { }Usage in service code:
private final Converter converter;
List<Order> orders = ...;
List<OrderDTO> result = converter.convert(orders, OrderDTO.class);4. Using Eager Loading to “Solve” Lazy Loading
Changing @OneToMany(fetch = FetchType.EAGER) forces all related data to be loaded on every query, quickly degrading performance. Keep lazy loading and apply @EntityGraph or DTO projection as shown above.
5. Large Transaction Spanning Remote Calls
Placing remote‑service calls inside a @Transactional method holds a database connection for the duration of the remote call and may cause a rollback even before any DB work.
@Transactional
public void createOrder(Order order) {
restClient.get().uri("/userinfo");
restClient.get().uri("/storage");
orderRepository.save(order);
}Separate the remote calls from the transactional block:
public void createOrder(Order order) {
restClient.get().uri("/userinfo");
restClient.get().uri("/storage");
orderService.doCreateOrder(order);
}
@Transactional
public void doCreateOrder(Order order) {
orderRepository.save(order);
}6. Pagination That Also Counts Total Rows
Standard Page<...> queries generate two SQL statements: one for the page data and one for the total count, which is costly on large tables.
Use Slice<...> to retrieve a page without the count query.
Slice<Order> findByCustomerName(String customerName, Pageable pageable);7. Logging Entity Objects Directly
Logging an entity with logger.info("Order {}", order) invokes toString(), which may trigger lazy loading and extra SQL queries.
Log only the required fields:
logger.info("Order id: {}", order.getId());Applying these practices eliminates common performance traps in Spring Boot applications that use Spring Data JPA.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
