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 Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
7 Common Spring Boot 3 Performance Pitfalls and How to Fix Them

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.

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.

dtoSpring BootjpaEntityGraph
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.