What is the difference between lazy and eager fetching, and what is LazyInitializationException?

9 minadvancedlazy-loadingeager-loadinglazyinitializationexception

Quick Answer

Eager fetching (FetchType.EAGER) loads an association immediately as part of the owning entity's own query, always available but at the cost of potentially fetching data that isn't needed. Lazy fetching (FetchType.LAZY) defers loading an association until it's actually accessed in code, which is more efficient by default but requires that access to happen while a persistence context (session) is still open — accessing a lazy association after the session has closed (e.g., after a @Transactional method has returned) throws LazyInitializationException.

Detailed Answer

JPA associations (@OneToMany, @ManyToOne, @OneToOne, @ManyToMany) can be configured to load either eagerly or lazily:

FetchType.EAGER — the association is loaded immediately, as part of fetching the owning entity itself (either via a join in the same query, or an additional query issued right away):

@ManyToOne(fetch = FetchType.EAGER) // default for @ManyToOne/@OneToOne
Customer customer;

Guarantees the data is always available, but risks fetching data that many callers of that entity don't actually need — and is itself a common contributor to the N+1 problem or unnecessarily wide queries if applied too liberally.

FetchType.LAZY — the association is only loaded when it's actually accessed in code, via a proxy object that transparently triggers a query the first time one of its methods is called:

@OneToMany(fetch = FetchType.LAZY, mappedBy = "order") // default for @OneToMany/@ManyToMany
List<LineItem> lineItems;

More efficient by default (you only pay for data you actually use), but comes with an important constraint: the lazy-load query can only succeed while the entity is still attached to an open persistence context (an active Hibernate Session/JPA EntityManager).

LazyInitializationException occurs when code tries to access a lazy association after that persistence context has already closed — most commonly, after a @Transactional service method has returned (closing the transaction, and with it the persistence context) and some later layer (a view template, a JSON serializer, a separate method) tries to access a lazily-loaded field on an entity that's now detached:

@Transactional(readOnly = true)
Order getOrder(Long id) {
    return repository.findById(id).orElseThrow();
} // transaction/session closes here

// later, e.g. in a view or serializer:
order.getLineItems().size(); // LazyInitializationException — session is already closed!

Common fixes:

  • Fetch the needed association eagerly for that specific query (JOIN FETCH/@EntityGraph) so it's already populated before the session closes.
  • Access and fully materialize the needed lazy data while still inside the transactional method, before returning.
  • Map the entity to a DTO (containing only the fields actually needed) inside the transactional boundary, and return the DTO instead of the raw entity — this is generally considered the cleanest, most robust long-term solution, since it also avoids leaking JPA entity/proxy internals out of the service layer entirely.
  • The historically common (but now generally discouraged) "Open Session in View" pattern keeps the persistence context open for the entire web request, masking the problem rather than fixing it, at the cost of harder-to-reason-about database connection usage.