Stage 7 - Fetch Types: LAZY, EAGER, and Loading Problems
Fetch type answers one question: should the relation be loaded now or only when code touches it? If an Order has a User, Hibernate can load the user immediately with the order or wait until code accesses order.getUser(). This decision affects performance, SQL count, transaction boundaries, and API design.
EAGER loading
FetchType.EAGER means the related entity should be loaded immediately. At first it feels convenient because the field is available without another explicit query. For small examples, this can look simpler.
The downside is that eager loading can bring data the current use case does not need. A list of orders may suddenly load users, profiles, roles, or other relationships. This increases memory usage and can produce large joins or many additional queries. Once a relationship is eager, every query for that entity pays the cost unless a more specific query changes the plan.
LAZY loading
FetchType.LAZY means the relation is loaded only when accessed. For example:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
When the order is loaded, Hibernate may store a proxy instead of the real user object. If code later calls order.getUser().getEmail() inside an open persistence context, Hibernate sends another SQL query and initializes the relation.
Lazy loading is usually the better default for collections and many relationships because it keeps the first query focused. The service can then explicitly choose what to load for a specific screen or endpoint using join fetch, entity graphs, or DTO queries.
| Endpoint scenario | What EAGER would do | What LAZY would do | Better decision |
|---|---|---|---|
| List 50 orders with only id and status | Load users even though the response does not need them. | Load only orders first. | Keep relation lazy and return an order DTO. |
| Show one order with customer email | Customer is available immediately, but every order query pays that cost. | Customer loads when accessed inside the transaction. | Use lazy mapping plus JOIN FETCH or DTO query for this endpoint. |
| Serialize entity after service returns | Related data may already be loaded, but the response shape is uncontrolled. | Lazy access can fail with LazyInitializationException. | Convert to DTO inside the service method. |
| Load a collection relation | Can create a very large query or multiple extra queries. | Avoids loading the collection until needed. | Prefer lazy collections and explicit fetching for use cases. |
LazyInitializationException
LazyInitializationException happens when code accesses a lazy relation after the persistence context is closed. Hibernate wants to load data, but it no longer has an active session connected to the database.
A common cause is returning entities directly from a service or controller and letting JSON serialization touch lazy fields later. The fix is not to make every relation eager. Better fixes are: load required data inside the service, convert entities to DTOs inside the transaction, use JOIN FETCH for a specific query, use @EntityGraph, or create a DTO projection.
What happens in SQL
With eager loading, one repository call may produce a join or extra select statements immediately. With lazy loading, the first select loads only the owner table. Later field access triggers another select for the related entity. SQL logging is the easiest way to see this. Use it while learning, but do not leave noisy SQL output as the only production observability tool.
Best practices
Prefer lazy relationships by default, especially for collections. Do not return JPA entities directly from public APIs. Design service methods around use cases and return DTOs. For each endpoint, decide exactly which fields and relations are needed. If a query needs related data, load it explicitly. This approach makes performance easier to reason about than relying on global eager mappings.
Practice
Create Order with a lazy User relation. Load a list of orders and observe that users are not loaded initially. Then access order.getUser().getEmail() inside a transaction and inspect the additional SQL. Try the same access after the transaction closes and observe LazyInitializationException. Fix it with a DTO query or JOIN FETCH.
Understanding checklist
- I can explain when EAGER and LAZY load data.
- I know why EAGER is not a universal fix.
- I can explain
LazyInitializationException. - I understand why DTOs are safer API responses than entities.
- I can verify fetch behavior through SQL logs.