A Spring Data repository method looks like one clean line, but the endpoint issues several hundred queries per request. How do you diagnose the N+1, and what would you actually change?
The repository call fetches parents in one query; the extra queries come later, when something touches a lazy association - usually the JSON serialiser, with open-session-in-view keeping the context alive so it succeeds silently. Diagnose by counting statements per request, then fix with a join fetch or entity graph, batch fetching, or a DTO projection.
What the interviewer is scoring
- Whether the candidate explains that the extra queries are triggered by association access, not by the repository method itself
- That they name open-session-in-view as the reason the lazy loads succeed instead of throwing LazyInitializationException
- Does the answer reach for a measurement - statement counts per request - before proposing a fix
- Whether they know join fetch plus pagination degrades to in-memory paging, and can cite the symptom
- That batch fetching is offered as the answer when several collections are involved, rather than stacking join fetches
- Whether a DTO projection is considered when the endpoint never needs managed entities
- That the candidate proposes a regression guard, such as asserting query count in a test
Answer
Short answer
The repository method is not issuing the queries. It fetches the parent rows in one statement and returns entities whose collections are lazy proxies. The extra queries appear later, when something iterates those collections — almost always the JSON serialiser walking the object graph. Open-session-in-view keeps the persistence context open into the view layer, so each lazy load quietly succeeds instead of failing, and one clean line becomes N+1 statements.
Why it is invisible in the code
// Looks like a single query.
List<Order> orders = orderRepository.findByCustomerId(id, PageRequest.of(0, 50));
return orders; // <-- the N+1 happens after this line, during serialisation
Reading this method will never reveal the problem, because the N queries are emitted by code nobody wrote: Jackson calls order.getLines(), Hibernate sees an uninitialised proxy, and it issues SELECT * FROM order_line WHERE order_id = ? — once per order. Fifty orders become fifty-one statements. Add a lazy customer on each line and it is worse.
The reason this fails silently rather than loudly is spring.jpa.open-in-view, which defaults to true. It holds the EntityManager open for the whole request, so lazy loads outside the service layer work. Turn it off and the same code throws LazyInitializationException at serialisation time — noisy, but honest. Spring Boot logs a warning about this default at startup precisely because it converts a visible error into an invisible performance problem.
Diagnose by counting, not by reading
The reliable move is to measure statements per request. Reading code for N+1 does not scale and misses the cases where the association is touched three layers away.
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=DEBUG
# and, to see the actual SQL with bound parameters, a proxy such as
# datasource-proxy or p6spy - not show-sql, which omits the bindings
Hit the endpoint once and read the statement count off the statistics line. If fifty orders produce fifty-one queries, you have found it without guessing. This also gives you the number to put in a regression test later, which matters because N+1 reappears the moment someone adds a field to a DTO.
Fixing one collection: join fetch or entity graph
For a single association, fetch it in the same statement:
@Query("select distinct o from Order o join fetch o.lines where o.customerId = :id")
List<Order> findWithLines(@Param("id") Long id);
or declaratively, which keeps the derived query and is usually the better default in Spring Data:
@EntityGraph(attributePaths = {"lines"})
List<Order> findByCustomerId(Long id);
Both turn N+1 into one query. @EntityGraph is preferable when you want the repository's derived method or its pagination behaviour; a hand-written JPQL query is preferable when the fetch plan and the filtering need to be reasoned about together.
The trap: join fetch with pagination
This is where the naive fix causes a worse problem, and interviewers ask about it because the symptom is confusing. Join-fetching a collection multiplies rows — fifty orders with ten lines each return five hundred rows — so the database can no longer apply LIMIT to give you fifty orders. Hibernate resolves this by fetching every matching row and paginating in memory, warning:
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory
Your page size is now decoration. On a large result set the heap goes with it. The correct pattern is to page the ids first and fetch the collections for that page in a second query, or to switch to batch fetching, which paginates correctly by construction.
Stacking two collection join fetches fails differently and immediately, with MultipleBagFetchException: cannot simultaneously fetch multiple bags — Hibernate refusing to produce the cartesian product of two collections.
Fixing several collections: batch fetching
When more than one association is involved, batch fetching is the tool that scales. Instead of one query per parent, Hibernate collects uninitialised proxies and loads them in batches with an IN clause:
spring.jpa.properties.hibernate.default_batch_fetch_size=50
Fifty orders now cost two queries rather than fifty-one — one for the orders, one for all their lines. It does not multiply rows, so pagination keeps working, and it applies globally rather than requiring every query to be annotated. For most applications, setting a sane default batch fetch size is the single highest-value change, because it fixes the N+1s nobody has found yet.
Often the real answer: stop fetching entities
If the endpoint returns a fixed JSON shape, it probably never needed managed entities. A projection selects exactly the columns the response contains, in one query, with no persistence context, no proxies, and no possibility of N+1:
public interface OrderSummary {
Long getId();
String getStatus();
BigDecimal getTotal();
}
List<OrderSummary> findByCustomerId(Long id); // Spring Data builds the projection
This is worth proposing explicitly, because a large share of N+1 problems are really a design mismatch — using the write model to serve a read endpoint. It also removes the open-session-in-view dependency for that path entirely.
What not to reach for
Changing the association to FetchType.EAGER looks like a fix and is a trap. It does eliminate the lazy load on this endpoint, at the cost of loading that collection on every query that touches the entity, including the ones that only needed the id. It converts a local problem into a global one, and it cannot be turned off per query. Fetch strategy belongs to the query, not to the mapping.
Keeping it fixed
Because N+1 regressions are silent, the durable answer includes a guard. Assert the statement count in an integration test — via Hibernate's Statistics.getPrepareStatementCount() or a datasource proxy — so that a future change which adds a lazy field to the response fails the build rather than quietly adding four hundred queries. Proposing that guard, rather than only the fix, is usually what separates a candidate who has debugged this once from one who has had to stop it coming back.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- You add JOIN FETCH and now the page size is ignored and heap usage climbs. What happened?
- Two collections on the same entity, both join fetched. What exception do you get and why?
- Would setting FetchType.EAGER on the association fix this? What does it do to your other queries?
- How do you decide between @EntityGraph and a hand-written JPQL query?
- What breaks if you simply disable open-session-in-view tomorrow?
Related questions
- Nobody called save, but the UPDATE still went to the database. Explain how that happened.mediumAlso on spring-data-jpa and hibernate5 min
- This endpoint logs one query per row. Why are you seeing N+1 queries, and how do you fix them?mediumAlso on hibernate and n-plus-one6 min
- You pass a thousand new entities to saveAll and the database sees a thousand separate inserts. How do you get them batched?hardAlso on spring-data-jpa and hibernate5 min
- A list endpoint that returns fifty orders issues more than a hundred queries, and rewriting it as an async endpoint made it slower. Explain both.hardAlso on n-plus-one5 min