When lazy beats eager, how Hibernate handles lazy loading outside transactions (and why open-session-in-view is a trap), accessing lazy fields after the session closes, pitfalls of EAGER in large graphs and Cartesian products, bytecode enhancement and lazy basic attributes, forcing initialization, how fetch types and fetch joins affect performance, detecting and fixing N+1, lazy loading in DTO projections, and lazy loading's impact on REST APIs.
Published September 25, 2026
Default everything to LAZY, and fetch per use case. That's the senior rule. The questions test whether you know why eager defaults cause N+1 queries and huge graphs, how proxies work, and the legitimate fixes: fetch joins, entity graphs, batch fetching, and DTO projections.
Short answer: Almost always, as the mapping default. Lazy loading defers loading an association until it's accessed, so each use case can decide what to fetch. Eager mappings load the association every time the entity is loaded, by any query, including in lists, where it multiplies into N+1 selects or huge joins. Eager fetching is appropriate only per query (fetch joins or entity graphs) when you know the association is needed. At the mapping level, it's rarely right: perhaps for a tiny, always-needed @ManyToOne to a cached reference entity.
Learn it in depth → The N+1 Query Problem
Short answer: Lazy associations are proxies (for @ManyToOne/@OneToOne) or persistent collections (PersistentBag/PersistentSet) that need an open session to load their data. Accessing one after the session is closed (outside @Transactional, in the controller, or during JSON serialisation) throws LazyInitializationException: could not initialize proxy – no Session.
The workarounds, and their costs:
spring.jpa.open-in-view=true, and logs a warning). It hides the problem, but it causes lazy loading during view or JSON rendering: unpredictable N+1 queries, database connections held for the whole request, and SQL issued from the presentation layer. Disable it (spring.jpa.open-in-view=false), and fetch explicitly.hibernate.enable_lazy_load_no_trans=true opens a temporary session per lazy load. It's an anti-pattern: hidden N+1 queries, no transactional consistency.Short answer: Load what you need inside the transaction, in the service layer:
select o from Order o join fetch o.lines where o.id = :id.@EntityGraph on repository methods.@BatchSize(size = 50), or hibernate.default_batch_fetch_size=50), which loads the lazy associations of many parents in IN (...) batches.Hibernate.initialize(order.getLines()).Short answer:
The fix: LAZY everywhere, plus explicit fetching per use case.
Short answer: Yes. Fetching two or more collections in one SQL query (through eager mappings or fetch joins) joins them all. An order with 10 lines and 5 payments returns 50 rows, duplicating the data. With more collections and more rows, the result grows multiplicatively, which is slow, and memory-hungry. Hibernate even refuses two bags (MultipleBagFetchException).
Fixes:
Sets (which avoid the exception, but not the row explosion);FetchMode.SUBSELECT;Short answer: Hibernate bytecode enhancement (through the Maven or Gradle plugin at build time, or a runtime agent) instruments entity classes, adding interception to field access. It enables:
@Basic(fetch = LAZY) on large columns (@Lob documents, JSON blobs), which otherwise can't be lazy. The hint is ignored without enhancement. Grouped with @LazyGroup;@OneToOne on the inverse side (without enhancement, the inverse one-to-one must be loaded eagerly, to know whether it's null);The trade-offs: build complexity, and debugging surprises. Many teams instead move large columns to a separate entity, or table, fetched on demand.
Short answer: Inside an open session or transaction:
Hibernate.initialize(order.getLines()) (or Hibernate.initialize(proxy));order.getLines().size()) works, but reads as a hack;join fetch, or an @EntityGraph), so it's loaded in the same SQL, instead of a second round trip.Check the state with Hibernate.isInitialized(...). After the session has closed, you'd need to reattach (merge) the entity, or re-query.
Short answer:
join fetch) override LAZY for that query: the association is loaded in the same SQL, and initialised in the persistence context, so the proxy becomes a real object. The caveats:
HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory), so for paging, fetch IDs first, then fetch the entities by those IDs;WHERE on the fetched collection without filtering the collection itself (that's semantically risky).Short answer:
assertSelectCount(2));join fetch or @EntityGraph for the use case;default_batch_fetch_size), which turns N+1 into 1 + N/50;@ManyToOne(fetch = LAZY) everywhere, to avoid eager-induced N+1.@Query("select distinct o from Order o join fetch o.customer left join fetch o.lines where o.status = :status")
List<Order> findWithCustomerAndLines(@Param("status") OrderStatus status); // 1 query instead of 1 + 2N
Short answer: DTO projections don't involve lazy loading at all. Constructor expressions, record projections and closed interface projections select exactly the listed columns through joins in the query, and produce plain objects, with no proxies or persistence context. So there's no LazyInitializationException, and no N+1, as long as the projection doesn't contain entity references. The caveats:
new Dto(o.customer)), that entity is managed and its lazy fields can still fail. Project scalar paths (o.customer.name) instead;@Value("#{target...}")) load the full entity, so lazy access in the expression can trigger queries.Short answer: Returning entities from controllers creates several problems:
LazyInitializationException.hibernateLazyInitializer fields, needing jackson-datatype-hibernate), and infinite recursion on bidirectional links.The practice: API DTOs assembled in the service layer, inside a transaction, from explicitly fetched data (projections or fetch joins). Disable OSIV, and document and test the query counts per endpoint.
Short answer: @ManyToOne and @OneToOne are EAGER. @OneToMany and @ManyToMany are LAZY. Basic attributes are eager (lazy needs enhancement). Override all the to-one associations to LAZY.
Q: Why does distinct appear in fetch-join queries?
A: A collection fetch join returns one row per child, so the parent appears several times in the result. select distinct deduplicates the parent entities. In Hibernate 6, this deduplication happens automatically in memory, and doesn't need to be passed to SQL.
Q: What is @Fetch(FetchMode.SUBSELECT)?
A: When one parent's collection is initialised, Hibernate loads the collections of all the parents from the original query, with one subselect (where parent_id in (select id from … original query …)). That's two queries in total.
Q: How do you paginate parents while also fetching their children efficiently?
A: Page over the parent IDs (a query with limit/offset, or keyset), then fetch those parents with join fetch on the collection, where p.id in :ids. Or use batch fetching for the children.
Q: Is FetchType.LAZY on @ManyToOne always honoured?
A: Yes, with proxies, as long as the target class isn't final and the ID is available. For the inverse side of @OneToOne, laziness needs bytecode enhancement, or @MapsId.