What are the fetch types, and which is the default for each mapping?
LAZY loads on first access, EAGER loads with the parent — and the defaults are not the same on both sides: @ManyToOne and @OneToOne default to EAGER, collections default to LAZY. Touching a lazy association after the session closes is LazyInitializationException, and Spring Boot's open-in-view setting hides that by keeping the session alive through rendering.
The Answer
Say this in the room. 45 seconds.
- LAZY loads the association the first time you touch it. EAGER loads it with the parent, every time the parent is loaded.
- The defaults differ by mapping, and this is the part people get wrong.
@ManyToOneand@OneToOnedefault to EAGER.@OneToManyand@ManyToManydefault to LAZY. - So loading one entity and touching nothing can still issue several queries, because every to-one association came along uninvited.
- Touching a lazy association after the persistence context has closed throws
LazyInitializationException— the association is a proxy, and the proxy has nothing to load from. - Spring Boot sets
spring.jpa.open-in-view=trueby default. That makes the exception disappear, and relocates the queries into the rendering phase, outside your transaction and outside anything you are timing. - Make everything LAZY, then fetch what you need per query. Better still, return a DTO so the boundary does not need a session at all.
Understand It
Everything below runs against a model of a persistence context, not Hibernate: a session that can open and close, proxies that throw when touched afterwards, and a query counter. Not modelled: SQL, the identity map, dirty checking, or bytecode enhancement — which is the one place the model would mislead, and the prose says where.
The defaults are not symmetric
// The defaults, which are not the same on both sides of a relationship.
for (String mapping : List.of("@ManyToOne", "@OneToOne", "@OneToMany", "@ManyToMany"))
System.out.printf(" %-12s defaults to %s%n", mapping, defaultFetchFor(mapping));
System.out.println();
// What that means when you load one order and touch nothing.
var session = new Session();
var order = new OrderEntity(session, defaultFetchFor("@ManyToOne"), defaultFetchFor("@OneToMany"));
System.out.println(" loading one Order and reading no association:");
System.out.println(" queries issued : " + session.queries);
System.out.println(" customer loaded: " + order.customer.isLoaded());
System.out.println(" items loaded : " + order.items.isLoaded());
session.log.forEach(sql -> System.out.println(" " + sql)); @ManyToOne defaults to EAGER
@OneToOne defaults to EAGER
@OneToMany defaults to LAZY
@ManyToMany defaults to LAZY
loading one Order and reading no association:
queries issued : 2
customer loaded: true
items loaded : false
select o from Order o
select order.customer -- eager, issued with the parentTwo queries to load one order, having asked for nothing. The customer came because @ManyToOne is eager unless you say otherwise, and almost nobody says otherwise — the annotation reads as a description of the relationship, not as a loading decision.
The asymmetry has a rationale: a to-one association is one row, so the designers assumed fetching it was cheap. That reasoning holds for one entity and fails immediately for a list. Load a hundred orders with three eager to-one associations and you have three hundred extra queries before any code has touched anything.
It also compounds. An eager @ManyToOne on an entity that itself has eager to-ones pulls in a graph you never named, and the only way to see how far it reaches is to count the queries.
Two things the model does not capture. Hibernate can often satisfy an eager to-one with a join rather than a second select, so the query count may be lower than shown here while the row width grows instead. And
FetchType.LAZYon a to-one is only a hint without bytecode enhancement — Hibernate needs a proxy it can substitute, and for a to-one it frequently ignores the hint. Neither changes the conclusion: eager to-ones are the associations that surprise people.
LazyInitializationException, and the setting that hides it
// Ten orders, and a view that renders each one's items.
System.out.println(" lazy, session closed when the service returns:");
System.out.println(" " + renderList(10, Fetch.LAZY, false, false));
System.out.println();
System.out.println(" lazy, with spring.jpa.open-in-view at its default of true:");
System.out.println(" " + renderList(10, Fetch.LAZY, true, false));
System.out.println();
System.out.println(" someone makes the collection EAGER instead:");
System.out.println(" " + renderList(10, Fetch.EAGER, false, false));
System.out.println();
System.out.println(" fetched deliberately in the service:");
System.out.println(" " + renderList(10, Fetch.LAZY, false, true)); lazy, session closed when the service returns:
could not initialize proxy [order.items] - no Session
lazy, with spring.jpa.open-in-view at its default of true:
1 queries in the service, 10 more while rendering
someone makes the collection EAGER instead:
11 queries in the service, 0 more while rendering
fetched deliberately in the service:
2 queries in the service, 0 more while renderingFour lines, four different answers to the same code.
The exception is the honest one. The service returned an entity whose collection was never loaded, the session closed, and the view asked for it. There is nothing to load from. It is an alarming-looking error that is telling you something true: the object you passed to the view is not fully formed.
The second line is why most people have never seen that exception. With open-in-view on, the session survives until the response is written, so the view's access succeeds — and issues ten queries doing it. The endpoint works. The N+1 is now happening after the transaction commits, outside the service method, in a phase most timing and most query-count assertions do not cover. Nothing is wrong except the ten queries nobody is looking at.
The third line shows why EAGER is not the fix. Eleven queries instead of ten plus one; the N+1 simply moved earlier, and it now happens on every query in the application that returns an Order.
The fourth is the actual answer. Two queries, both inside the service, both deliberate.
The point to carry out of this is that open-in-view does not solve the problem it appears to solve. It converts a loud failure into a quiet cost, and it does so by default, which is why so many Spring Boot applications have an N+1 in their rendering phase that nobody has ever measured.
Reference
The correct implementation, the configuration, and the migration path. Copy from here.
The defaults, and what to write instead
| Mapping | JPA default | Write this |
|---|---|---|
@ManyToOne | EAGER | @ManyToOne(fetch = FetchType.LAZY) |
@OneToOne | EAGER | @OneToOne(fetch = FetchType.LAZY) — see the caveat below |
@OneToMany | LAZY | leave it |
@ManyToMany | LAZY | leave it |
@Entity
class Order {
@ManyToOne(fetch = FetchType.LAZY) // never rely on the default
private Customer customer;
@OneToMany(mappedBy = "order") // already lazy
private List<Item> items;
}
Make every to-one lazy as a matter of course. It is the only decision that can be made once, in the mapping, without knowing what future queries will need — because a lazy association can always be fetched by a query that wants it, and an eager one cannot be un-fetched by a query that does not.
The @OneToOne caveat. On the non-owning side, LAZY does not work: Hibernate has to know whether the row exists to decide between a proxy and null, so it queries anyway. Making the association optional-free (@OneToOne(optional = false)) or putting the foreign key on this side both help. If it matters, bytecode enhancement is the real fix.
Fetching deliberately, per query
// The association stays lazy; this one query says what it needs.
@EntityGraph(attributePaths = { "items", "customer" })
List<Order> findByStatus(Status status);
@Query("select distinct o from Order o join fetch o.items where o.id = :id")
Optional<Order> findWithItems(@Param("id") Long id);
Turning off open-in-view
# Spring Boot's default is true. Turn it off and find out what breaks.
spring.jpa.open-in-view=false
Turning it off is the recommended setting and is not a free change: every place that was quietly lazy-loading during rendering now throws LazyInitializationException. That is the point — each exception is a place that was issuing unplanned queries. Do it in a non-production environment first, fix each one by fetching deliberately or returning a DTO, and expect to find more than you think.
Spring Boot logs a warning about this on startup precisely because the default is convenient and wrong for most applications:
spring.jpa.open-in-view is enabled by default. Therefore, database queries may be
performed during view rendering. Explicitly configure spring.jpa.open-in-view to
disable this warning
The fix that removes the question
// If the boundary takes a DTO, there is no session to be open or closed and
// no proxy to initialise. The whole class of problem disappears.
public record OrderView(Long id, String reference, String customerName, int itemCount) {}
@Query("""
select new com.example.OrderView(o.id, o.reference, c.name, size(o.items))
from Order o join o.customer c
where o.status = :status
""")
List<OrderView> findViews(@Param("status") Status status);
Most LazyInitializationExceptions are a signal that an entity escaped further than it should have. Entities are for a transaction that intends to write; a read endpoint is usually better served by a query shaped like its response.
Choosing quickly
| Situation | Do |
|---|---|
Any @ManyToOne / @OneToOne | fetch = LAZY, always, then fetch per query |
| A screen that needs one association | @EntityGraph on that repository method |
| A screen that needs a few columns | A DTO projection; no entity, no session question |
| A paged list with collections | Page ids, then fetch collections for those ids |
| Anything at all | spring.jpa.open-in-view=false |
Scenarios
Real situations, with the decision and the argument.
1. LazyInitializationException in production, on a page that worked yesterday.
Something moved the association access outside the transaction, or something closed the session earlier — a @Transactional removed, a method extracted, a serialiser reaching a field the previous one skipped.
Resist the two quick fixes. Making the association eager will fix this page and slow every other query returning that entity; adding @Transactional to the controller keeps the session open across the whole request, which is open-in-view by another name. Fetch what the endpoint needs in the service, with an entity graph, or return a DTO. The exception is pointing at a real design problem — an unformed object crossing a boundary — and both quick fixes silence it without addressing that.
2. An endpoint is slow and the service method looks fine — one query, measured.
Measure the whole request rather than the service method. With open-in-view on, lazy loads during rendering happen after your @Transactional has committed and after most timing instrumentation has stopped, so a service that measures at one query can be issuing fifty.
Set spring.jpa.open-in-view=false in a test environment and run the endpoint. If it now throws, you have found exactly where the extra queries were coming from.
3. Someone proposes making all associations EAGER so the exceptions stop.
It will stop the exceptions and make the system slower everywhere. Eager is a property of the mapping, so it applies to every query that returns the entity, including the ones that never touch the association — and eager to-ones chain, pulling in a graph nobody named.
The counter-proposal is concrete rather than principled: keep them lazy and add an entity graph to the specific repository method. That fixes the same endpoint, changes nothing else, and is visible at the call site where the next reader will look for it.
4. Turning off open-in-view breaks fifteen endpoints.
That is the setting doing its job. Each break is a place that was issuing unplanned queries during rendering, and the only thing that changed is that they are now visible.
Work through them rather than reverting. Most will be a missing @EntityGraph or an entity that should have been a DTO, and they are usually quick. If the number is genuinely unmanageable, turn it off per-environment first and fix the endpoints in order of traffic — but do not treat the count as evidence the setting was wrong, because that count is the finding.
5. A @OneToOne(fetch = LAZY) still loads eagerly.
Expected on the non-owning side. Hibernate has to know whether the related row exists in order to decide between a proxy and null, and it cannot know that without querying, so the hint is ignored.
Three options, in order of how much you want it: mark it optional = false if the relationship genuinely always exists, put the foreign key on this side so it becomes the owner, or enable bytecode enhancement. Worth knowing as a specific fact, because "I set LAZY and it didn't work" is otherwise a genuinely confusing afternoon.
Interviewer's Next Move
The question after the question. This is what the round is actually testing.
1. "What are the fetch types?" LAZY loads the association on first access, EAGER loads it with the parent. The interesting part is the defaults, which are not the same for both sides.
2. "What are the defaults?"
@ManyToOne and @OneToOne are EAGER; @OneToMany and @ManyToMany are LAZY. So loading one entity that touches nothing can still issue several queries, and a list of a hundred can issue hundreds.
3. "Why did they choose that?" A to-one is one row, so fetching it was assumed cheap. That holds for a single entity and breaks immediately for a list, which is why the recommendation now is to override it everywhere.
4. "What causes LazyInitializationException?"
Touching a lazy association after the persistence context closed. The field is a proxy with nothing to load from — usually because the entity escaped the service layer and the view asked for something the service never fetched.
5. "How do you fix it?" Fetch what the endpoint needs while the session is open — an entity graph or a fetch join — or return a DTO so the boundary never holds an entity. Not by making the association eager, and not by widening the transaction.
6. "What does spring.jpa.open-in-view do?"
Keeps the persistence context open until the response is written, so lazy loads during rendering succeed. Spring Boot enables it by default, which is why most developers rarely see the exception — and why many applications have an N+1 in their rendering phase that no timing covers.
7. "Should you turn it off?" Yes, and expect it to break things. Each break is a place that was issuing unplanned queries. Turning it off makes the cost visible instead of removing it, which is what you want before deciding what to fix.
8. "Why doesn't @OneToOne(fetch = LAZY) work on the non-owning side?"
Hibernate must know whether the row exists to choose between a proxy and null, and it cannot know without querying. Fix it with optional = false, by owning the foreign key on that side, or with bytecode enhancement.
9. "Lazy or eager as a default policy?" Lazy, everywhere, then fetch deliberately per query. A lazy association can be fetched by a query that needs it; an eager one cannot be skipped by a query that does not.
Code traps
Trap A — predict before you run:
@Entity
class Order {
@ManyToOne private Customer customer;
@ManyToOne private Address shippingAddress;
@OneToMany(mappedBy = "order") private List<Item> items;
}
List<Order> orders = repository.findAll(); // 500 orders
Answer
Up to 1,001 queries before any of your code touches an association. Both @ManyToOne mappings default to EAGER, so each of the 500 orders drags in a customer and an address; items is lazy and costs nothing until read.
The annotations look like a description of the data model, and two of the three are also loading decisions. That is the whole trap: nothing here says EAGER.
Hibernate may satisfy some of these with joins rather than separate selects, which lowers the query count and widens the rows instead — so the fix is the same either way. Add fetch = FetchType.LAZY to both to-one mappings and fetch what each query needs.
Trap B:
@Transactional
public OrderDto get(Long id) {
Order order = repository.findById(id).orElseThrow();
return mapper.toDto(order); // mapper reads order.getItems()
}
Answer
This one is correct, and it is worth being able to say why — the mapping happens inside the transaction, so the lazy load succeeds and the DTO leaves fully formed. The exception only appears when mapping moves outside, which is exactly what happens when someone "tidies up" by returning the entity and mapping in the controller.
The remaining cost is the N+1 if get is ever called in a loop, and the fix is an entity graph on the repository method rather than anything here.
The general shape to recognise: convert to a DTO inside the transaction and the whole class of lazy-loading problem stops existing at the boundary.
Trap C:
@RestController
class OrderController {
@GetMapping("/orders/{id}")
public Order get(@PathVariable Long id) {
return repository.findById(id).orElseThrow();
}
}
Answer
Returning the entity means Jackson serialises it, and Jackson touches every getter — including the lazy ones. With open-in-view on it works and silently issues a query per association; with it off it throws while serialising, producing a half-written response body and a confusing stack trace.
A bidirectional relationship makes it worse: Order → Item → Order is an infinite loop that only stops when the stack does.
Return a DTO. This is the single most common cause of LazyInitializationException in Spring applications, and the annotations people reach for — @JsonIgnore, @JsonManagedReference — are patches for a boundary that should not have had an entity on it.
Common wrong answers
| Said in interviews | Reality |
|---|---|
| "Everything defaults to LAZY." | To-one associations default to EAGER. |
| "EAGER is safer." | It applies to every query for that entity, including ones that do not need it. |
"LazyInitializationException means a Hibernate bug." | It means an entity outlived its session. |
"Add @Transactional to the controller." | That is open-in-view by another name. |
| "open-in-view fixes lazy loading." | It hides the exception and relocates the queries to rendering. |
| "Spring Boot defaults it to false." | It defaults to true, and warns about it on startup. |
"fetch = LAZY always works." | Not on the non-owning side of @OneToOne without enhancement. |
| "Serialising an entity is fine." | Jackson touches every getter, including the lazy ones. |
Check Yourself
Q1. You load one entity and touch none of its associations. Can that issue more than one query?
Answer
Yes, and usually does. @ManyToOne and @OneToOne default to EAGER, so every to-one association is fetched with the parent whether or not anything reads it — and eager to-ones on those entities chain further. Only @OneToMany and @ManyToMany default to LAZY. The practical rule that follows is to write fetch = FetchType.LAZY on every to-one mapping rather than relying on a default that was chosen for the single-entity case.
Q2. An endpoint measures one query in the service method and issues fifty per request. How?
Answer
spring.jpa.open-in-view is at its default of true, so the persistence context stays open until the response is written. Lazy loads during serialisation or template rendering therefore succeed — after the transaction has committed and after most timing and query-count instrumentation has stopped. Setting it to false makes those loads throw, which is how you find them.
Q3. Why is making the association EAGER the wrong response to a LazyInitializationException?
Answer
Because fetch type is a property of the mapping, not of the query. Setting EAGER fixes the one endpoint that threw and adds a fetch to every other query in the application that returns that entity, including the many that never touch the association — and eager to-ones pull in further eager to-ones. The exception is really reporting that an unformed entity crossed a boundary; fetch what that endpoint needs with an entity graph, or return a DTO so the boundary holds no entity at all.
Practice
| Tier | Exercise | Time |
|---|---|---|
| Warm-up | Find the queries you did not ask for | 10 min |
| Challenge | Six mappings, and what each costs | 25 min |
| Production | The endpoint that measured one query | 45 min |
| Interview | Full round replay | 10 min |
Practice ladder
Reading this page is not knowing it. Four tiers, ending in a real incident.
- Warm-up10 min
Find the queries you did not ask for
One concept, guided. Near-impossible to fail.
- Challenge25 min
Six mappings, and what each costs
Edge cases. You have to reason, and two valid fixes differ.
- Production incident45 min
The endpoint that measured one query
A real incident: symptom first, cause hidden, tradeoff at the end.
- Interview replay10 min
Full round replay — fetch types
Timed verbal replay with pass/fail criteria per follow-up.
Where this question goes next
- spring entity at the boundary — not written yet
- spring fetch join pagination — not written yet
- What is dirty checking, and why did my entity save without a save() call?
Questions that lead here
What is dirty checking, and why did my entity save without a save() call?
The persistence context keeps a snapshot of every entity it loads and compares it against the live object at flush time, issuing an UPDATE for anything that differs. Mutating a fetched entity is therefore a write, whether or not you called save. It also means the session holds a second copy of everything you load, and by default updates every column rather than the one you changed.
Asked constantlyintermediate1–15 yrs11 min readJpa hibernateWhat is the N+1 problem, and how do you detect it?
One query for the parents, then one more per parent when a lazy collection is touched — so twenty rows cost twenty-one queries and a thousand cost a thousand and one. You detect it by counting queries, never by reading code, because the loop that costs one query and the loop that costs a thousand are identical to read.
Asked constantlyintermediate1–15 yrs12 min readJpa hibernate
Every runnable example above was compiled and executed against openjdk 21.0.11 on this build, and its output diffed against what this page claims. Last updated 2026-08-30.