Hibernate lets you avoid loading related objects right away — you can pull them in later, when you actually need them. Convenient, but easy to get wrong: you end up with either extra queries or a mysterious error.
A proxy is a stand-in object with the same methods: the identifier is there from the start, the other fields are pulled in on first access. While the session is open that is just one extra query; once it is closed, the same call turns into a LazyInitializationException.
Two loading modes
Mapping an association with @OneToMany, @ManyToOne, @OneToOne or @ManyToMany, you can specify fetch:
@Entity
public class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}
FetchType.EAGER — the related object is loaded together with the parent: either a JOIN in the same query or a separate SELECT right after. A single query is not guaranteed.
FetchType.LAZY — the association is not loaded until first access. In its place Hibernate puts a proxy — an empty stub that knows only the identifier. The actual SQL runs later, on first access to any field except the identifier: that one the proxy knows from the start, so getId() never goes to the database — handy when all you need is the identifier of the related entity.
Short formula: LAZY means "load it when asked", EAGER means "load it right away".
How a proxy works
For single-valued associations (@ManyToOne, @OneToOne) Hibernate creates a proxy subclass of your entity — from the outside it looks like a regular object:
Order order = entityManager.find(Order.class, 1L);
// SQL: SELECT * FROM orders WHERE id = 1
// customer is NOT loaded yet
String email = order.getCustomer().getEmail();
// this is where Hibernate runs a second SELECT * FROM customer WHERE id = ?
For collections (@OneToMany, @ManyToMany), instead of a List or Set Hibernate substitutes its own implementation — PersistentBag, PersistentSet and others. They are also empty until first accessed.
The mechanics are visible without a database: the object knows its identifier and goes for everything else on first access.
live example
public class LazyProxyDemo {
static int queries = 0;
static class CustomerProxy {
private final long id;
private String email;
private boolean sessionOpen = true;
CustomerProxy(long id) {
this.id = id;
}
long getId() {
return id;
}
String getEmail() {
if (email == null) {
if (!sessionOpen) {
throw new IllegalStateException("LazyInitializationException: no Session");
}
queries++;
email = "user" + id + "@shop.example";
}
return email;
}
}
public static void main(String[] args) {
CustomerProxy customer = new CustomerProxy(7);
System.out.println("after find(Order): queries " + queries);
System.out.println("getId() = " + customer.getId() + ", queries " + queries);
System.out.println("getEmail() = " + customer.getEmail() + ", queries " + queries);
CustomerProxy closed = new CustomerProxy(8);
closed.sessionOpen = false;
try {
closed.getEmail();
} catch (IllegalStateException e) {
System.out.println("session closed: " + e.getMessage());
}
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
The counter is the point: getId() leaves it alone, getEmail() bumps it by one, and on a closed session the call blows up.
LazyInitializationException: what it is and why it happens
The most common error of lazy loading:
org.hibernate.LazyInitializationException:
failed to lazily initialize a collection of role: Order.items,
could not initialize proxy - no Session
The cause: you accessed a lazy association after the Hibernate session was closed. The proxy knows it has to load the data, but there is no open session left.
A typical scenario in Spring:
// Transaction is open — load the Order
@Transactional
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
} // <-- transaction is closed here
// Later, outside the transaction:
Order order = orderService.getOrder(1L);
order.getItems().size(); // BOOM: LazyInitializationException
The Hibernate session lives within the transaction: once the @Transactional method finishes, the session is closed and the proxy is "dead".
Mind you, a plain Spring Boot web application won't reproduce this: Open Session In View is on by default, so touching a lazy field in a controller quietly works and merely adds a query. The error shows up in a background job, a test, a queue consumer — or in the web layer once spring.jpa.open-in-view is off.
How to fix it properly
1. Load the data you need inside the transaction with JOIN FETCH
The cleanest way is to state what you need right in the query:
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
Hibernate runs one SQL with a JOIN and returns a fully initialized object — the data is already there.
2. Load via EntityGraph
An alternative to JPQL is @EntityGraph from Spring Data JPA:
@EntityGraph(attributePaths = {"items"})
Optional<Order> findById(Long id);
A flexible option when the same repository method is needed with different sets of associations. More in Spring Data JPA.
3. Access associations inside the transaction
If business logic needs data from an association, let that happen inside the same transaction:
@Transactional
public int countItems(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
return order.getItems().size(); // OK: the session is still open
}
Why you shouldn't put EAGER everywhere
First impulse: "I'll set EAGER and forget about the error". That's a trap.
Problem 1: extra data every time. Even when you only need the order's identifier, Hibernate loads all of its items — EAGER means "always".
Problem 2: N+1 queries or a Cartesian product. With 100 orders, each holding an EAGER collection of items, Hibernate either runs 100 extra SELECTs (N+1), or does a JOIN and returns rows with duplicates. More on N+1 in the N+1 problem in Hibernate article.
Problem 3: unexpected chains. EAGER on one association pulls an EAGER chain further along the graph, and a single find() ends up loading half the database.
Rule: keep LAZY by default and name what you need in the query.
Reference for JPA defaults:
| Annotation | Default |
|---|---|
@ManyToOne | EAGER |
@OneToOne | EAGER |
@OneToMany | LAZY |
@ManyToMany | LAZY |
@ManyToOne and @OneToOne default to EAGER — a historical accident. Change them explicitly to LAZY:
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
Open Session in View: don't rely on it
In Spring Boot, Open Session in View (OSIV) is on by default — it keeps the Hibernate session open for the whole HTTP request, including template rendering.
This lets you touch lazy associations outside @Transactional — no error, and it seems like "everything works". You pay with hidden SQL in the presentation layer: a controller or template calling order.getItems() quietly goes to the database.
To disable OSIV:
spring:
jpa:
open-in-view: false
After disabling it, LazyInitializationException starts showing up wherever data wasn't loaded explicitly — a good thing: the behaviour becomes visible. Load the associations you need inside the transaction instead of relying on an open session as a "safety net".
In short
FetchType.LAZY— a proxy, data loaded on first access;EAGER— loaded immediately.@ManyToOneand@OneToOnedefault to EAGER — change them to LAZY explicitly.LazyInitializationException— access to a proxy after the session is closed.- The right fix — load the associations you need inside the transaction:
JOIN FETCHin JPQL or@EntityGraph. - Don't put EAGER everywhere: it means hidden extra queries and a risk of N+1.
- OSIV hides the problem but doesn't solve it — disable it and load explicitly.
Further reading
- N+1 problem in Hibernate — how lazy loading in a loop spawns an avalanche of queries.
- Persistence context and the entity lifecycle — how the Hibernate session manages object state.
- Common Hibernate pitfalls — frequent traps when working with an ORM.
- Spring Data JPA — repositories,
@EntityGraphand other abstractions on top of Hibernate.