Prevent JPA N+1 Queries and Lazy Loading Traps in Spring Boot
Flags lazy JPA associations loaded once per row in loops and serializers, reliance on open-in-view, and EAGER mappings that cause N+1 queries.
What it flags
@Service
public class OrderReportService {
private final OrderRepository orders;
public OrderReportService(OrderRepository orders) {
this.orders = orders;
}
@Transactional(readOnly = true)
public List<OrderSummary> recentOrders(LocalDateTime since) {
return orders.findByCreatedAtAfter(since).stream()
// Loads customer and lines with separate queries for each order
.map(order -> new OrderSummary(
order.getId(),
order.getCustomer().getName(),
order.getLines().size()))
.toList();
}
}public interface OrderRepository extends JpaRepository<Order, Long> {
// Same method the report calls: customer and lines now load in one query
@EntityGraph(attributePaths = {"customer", "lines"})
List<Order> findByCreatedAtAfter(LocalDateTime since);
@Query("""
select o from Order o
join fetch o.customer
left join fetch o.lines
where o.status = :status
""")
List<Order> findByStatusWithDetails(@Param("status") OrderStatus status);
}Why it matters
An N+1 pattern costs one query per row, so an endpoint that feels instant against a development database with ten orders issues thousands of queries once a customer has thousands. Spring Boot enables open-in-view by default, which hides the problem: lazy loads keep working in controllers and Jackson serializers, and nothing fails until latency and connection pool usage climb. Reviewers miss it because each extra query hides behind a getter that reads like a field access.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Prevent JPA N+1 Queries and Lazy Loading Traps in Spring Boot Applies to `**/*.java`, `**/application*.yml`, `**/application*.yaml`, `**/application*.properties`. Review Spring Data JPA repositories, services, and response mapping for queries that run once per row and lazy loads that happen outside the query that needs them. Flag changes that: - Read a lazy association, such as `order.getLines()` or a `LAZY` `@ManyToOne`, inside a loop, stream, or mapping over a query result that did not fetch it with `JOIN FETCH`, `@EntityGraph`, or a DTO projection. - Serialize JPA entities that have lazy associations to JSON, so Jackson triggers the loads while rendering the response. - Rely on `spring.jpa.open-in-view` (enabled by default) to make lazy loading work in controllers or views, or set `hibernate.enable_lazy_load_no_trans` to silence `LazyInitializationException`. - Map a collection with `fetch = FetchType.EAGER`, or leave `@ManyToOne` and `@OneToOne` at their EAGER default on entities that are loaded in lists. - Combine a collection `JOIN FETCH`, or a collection attribute in `@EntityGraph`, with a `Pageable`, which makes Hibernate load every matching row and paginate in memory. - Call `findById` or a derived finder inside a loop over IDs instead of one `findAllById` or `IN` query. Do not flag: - Lazy access on a single loaded entity, such as a detail endpoint that reads one association once. - Loops over results already fetched with `JOIN FETCH` or `@EntityGraph`, or covered by `@BatchSize` or `hibernate.default_batch_fetch_size`. - Small bounded result sets, such as a top-10 query or a fixed lookup table.
Checks files matching **/*.java, **/application*.yml, **/application*.yaml, **/application*.properties.
More Spring Boot agents
See all Spring Boot code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.