Under The Hood
backend springboot Spring Data JPA 3.x / Hibernate 6.x

Spring Data JPA: N+1 Problem, JOIN FETCH, and @EntityGraph

Last updated
Prerequisites:
Spring Data JPA repositories
Understanding of JPA entities and relationships
  • spring
  • jpa
  • hibernate
  • n-plus-one
  • entitygraph
  • join-fetch
  • performance

Read at your depth

The practical view

The N+1 problem: loading N entities then accessing a lazy relationship on each causes N additional queries. Example: for (Order o : orderRepo.findAll()) { o.getCustomer().getName(); } — with lazy loading, each getCustomer() fires a query. Fixes: JOIN FETCH in JPQL (select o from Order o join fetch o.customer), @EntityGraph on the repository method (@EntityGraph(attributePaths = "customer")), or fetch joins in criteria queries. @EntityGraph is the cleanest in Spring Data: it tells the query to eagerly fetch the specified associations using a join (or a secondary select).

The same idea in other frameworks

react equivalent
N/A — server-side JPA concern.

Legacy vs modern

Lazy N+1 access vs @EntityGraph fetch

Accessing lazy associations in a loop fires one query per row; @EntityGraph/join fetch loads the graph in a single query.

before → after
Lazy access in a loop (N+1)
List<Order> orders = orderRepo.findAll(); // ❌ OLD: 1 query
for (Order o : orders) {
  Customer c = o.getCustomer(); // ❌ OLD: 1 query per order → N+1
}
@EntityGraph single-query fetch
@EntityGraph(attributePaths = "customer")
List<Order> findAll(); // ✅ NEW: one query with a join

Interview gotchas

Context

This tests the lazy-loading mechanics — interviewers want to know that accessing a lazy proxy is what fires the query, not the entity loading itself, and why loops amplify it.

The mechanical answer

JPA (Hibernate) represents lazy associations as proxies or lazy collections; the association data is NOT loaded when the parent is fetched. The first time you ACCESS the association (call getCustomer(), iterate a collection, check its size), Hibernate initializes the proxy — executing a new SELECT against the session (if the session is open and the entity is detached, it throws LazyInitializationException instead). In a loop over N parents, each parent's association access fires its own query, giving 1 + N queries. This is exponential with depth (accessing a relationship of a relationship multiplies further). The problem is fundamentally about access patterns, not just configuration — which is why the fix is to fetch the graph up front (JOIN FETCH / @EntityGraph) rather than relying on lazy defaults.

Trap

The naive answer is 'N+1 happens because relationships are lazy'. The trigger is the ACCESS pattern, not laziness itself; even eager EAGER fetch can cause joins that multiply rows. Another trap: thinking the session stays open outside the transactional boundary — in a web request without OpenSessionInView, accessing a lazy association after the transaction ends throws LazyInitializationException, a separate but related failure. Candidates who describe proxy initialization and access-triggered queries show real JPA understanding.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.