Spring Data JPA is one of those things where almost everyone hits this exception at least once, right?
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.Order#1] - no Session
You fix the mapping, you review the SQL, and it still won’t go away. That makes sense, because this isn’t a mistake in your mapping or query—it’s a problem of when you access the data. In this article, we’ll first understand why it happens in terms of Hibernate’s session and transaction boundaries, then lay out four proper approaches to fix it permanently. Along the way, we’ll also organize the reasoning for why you shouldn’t fall back on the easy open-in-view=true or switching to EAGER.
What Is LazyInitializationException - Start by Reading the Exception Message
For associations like @OneToMany or @ManyToOne(fetch = LAZY), Hibernate substitutes a proxy (or a PersistentCollection for collections) instead of the real entity. The actual data is fetched from the DB the moment you first touch that field.
At the time of this first access, if the Hibernate session (the persistence context) has already closed, there’s no way to query the DB, so the exception is thrown. Break the message down and it reads plainly.
could not initialize proxy… tried to resolve an uninitialized proxyno Session… but there was no session
In other words, it’s a state of “I want to go fetch the data, but the window I’d use to fetch it is already closed.” The cause of the error isn’t the correctness of your code—it’s that you touched it too late.
Why It Happens - The Session Lifecycle and Transaction Boundaries
By default, the Hibernate session lives tied to the scope of @Transactional. When the transaction begins, the session opens; when you exit the method and it commits, the session closes.
@Service
public class OrderService {
@Transactional
public Order findOrder(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
// Inside the transaction here = the session is open
order.getItems().size(); // Can be initialized
return order;
}
}
The problem is when you touch the order returned by this method in a Controller or in Thymeleaf.
@GetMapping("/orders/{id}")
public String show(@PathVariable Long id, Model model) {
Order order = orderService.findOrder(id);
model.addAttribute("order", order);
return "order/show"; // Accessing order.items in the template → exception
}
By the time you’re back in the Controller, the transaction is over and the session is closed. The moment the template tries to render order.items, it attempts to initialize the proxy on a closed session and blows up. The mismatch between where it gets initialized and where it gets referenced—this is the essence of the exception.
Isolating the Cause - Which Pattern Is Your Case
First, read the stack trace. It tells you which entity’s which association blew up, as in could not initialize proxy [com.example.Order#1]. Next, check whether that access is happening outside the transaction.
The situations that typically end up outside the transaction are:
- Association access during template rendering, such as in Thymeleaf
- The moment Jackson serializes an entity returned from a
@RestController - Post-processing in utilities outside the Service, or inside the Controller
What all of these have in common is “touching a Lazy association after exiting the transaction.” Once you’ve isolated it, the direction of the solution splits in two: move the initialization earlier, or fetch it in the shape you need in the first place. Let’s look at the four proper approaches from here.
Solution 1: Fetch the Required Associations Together with JOIN FETCH
If you join and fetch the association at query time, the returned entity has its proxies already resolved. It becomes safe to touch even outside the transaction.
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("select o from Order o join fetch o.items where o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
}
This suits cases where you need the association only for that specific use case. One thing to watch out for: if you combine a collection join fetch with paging (Pageable), Hibernate ends up loading all records into memory before paging, and you’ll get a warning. If listing plus paging is involved, consider @EntityGraph or DTO projection, covered next.
Solution 2: Fetch Declaratively with @EntityGraph
Without writing JPQL, you can specify the fetch scope on a per-repository-method basis. It’s easy to bolt onto method-name queries and highly readable, which is its advantage.
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"items", "customer"})
Optional<Order> findWithGraphById(Long id);
}
You just list the associations you want to fetch together in attributePaths. Remembering that JOIN FETCH’s strength is the flexibility to write complex conditions, while @EntityGraph’s strength is being declarative and easy to reuse, makes it easy to choose between them. For a detailed discussion of fetch strategies themselves, I’ll defer to the article on Spring Boot JPA entity relationship mapping.
Solution 3: Fetch Only the Data You Need with DTO Projection
The idea here is to not carry entities or proxies around across layers in the first place. If you convert only the fields you need into a flat DTO at query time, lazy proxies never exist to begin with.
public interface OrderSummary {
Long getId();
String getCustomerName();
int getItemCount();
}
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("select o.id as id, o.customer.name as customerName, size(o.items) as itemCount from Order o")
List<OrderSummary> findSummaries();
}
Since the DTO contains no proxies, no exception occurs even when referenced outside the transaction. For display or API-return purposes, you also avoid over-fetching entities, and it’s healthy from a separation-of-concerns standpoint. The different ways to write projections are gathered in Spring Data JPA DTO projections and interface projections. If you want to eliminate the exception at its root, this is the top candidate.
Solution 4: Apply @Transactional Appropriately and Initialize in the Service Layer
If you don’t want to significantly change your existing structure, complete the Lazy access inside the transaction boundary. It’s safest to touch and initialize the necessary associations in the Service layer, and return an already-initialized DTO to the Controller.
@Service
public class OrderService {
@Transactional(readOnly = true)
public OrderDto getOrder(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
return new OrderDto(order.getId(),
order.getItems().stream().map(ItemDto::from).toList());
}
}
The key is to not pass the entity as-is to the Controller. If you design it to “touch it before it closes,” you’ll no longer accidentally trip over a proxy outside the boundary. For details on propagation and readOnly, refer to Spring Boot transaction management.
Why You Shouldn’t Plug It with open-in-view=true
Setting spring.jpa.open-in-view=true (which is actually true by default) keeps the session open until view rendering, so the exception does indeed disappear.
# Recommended practice: explicitly turn it off and stay conscious of boundaries
spring.jpa.open-in-view=false
However, Spring Boot emits a warning about this setting at startup. It becomes hard to notice when implicit DB access or N+1 occurs in the view layer, and since it holds onto the DB connection until the response is complete, there are resource-side side effects too. You should see it not as “the error went away, so it’s solved,” but merely as hiding where you’re touching the DB. As a rule, set it to false, stay conscious of the boundary, and handle things with the four approaches above.
Why You Shouldn’t Escape by Switching to EAGER
If you make an association FetchType.EAGER, the exception stops appearing. But because EAGER always goes to fetch that association, it invites unnecessary joins and N+1 even in listings or batches that don’t need it. To avoid an exception in one place, you permanently raise the query cost across the entire application.
Fetching only where you need it (JOIN FETCH / @EntityGraph / DTO projection) is far easier to control. The N+1 and performance angle is covered in detail in Spring Data JPA performance optimization.
Choosing Among the Four Proper Approaches - Decision Criteria by Situation
When in doubt, choosing by use case is the simplest.
- Associations are needed together for screen display … JOIN FETCH or @EntityGraph
- Only limited fields are needed for API returns or listings … DTO projection is the strongest choice
- You want to close safely without significantly changing existing logic … initialize inside
@Transactionaland return a DTO open-in-view=trueand switching to EAGER … avoid as a rule, and only choose when you can explain why
Preparing separate repository methods for display and for the API is perfectly normal. The trick is not to force a single entity fetch to be reused everywhere.
Summary
The essence of LazyInitializationException is that it “tried to initialize a proxy after the session closed.” Isolating it is just a matter of looking at “where you’re doing the Lazy access, and whether the transaction is open at that moment.” For a permanent fix, choose according to use case from JOIN FETCH / @EntityGraph / DTO projection / placing @Transactional appropriately.
open-in-view=true and switching to EAGER only hide the error, and you pay the price in performance and clarity. Don’t adopt them lightly—fetch only what you need, in the shape you need, where you need it. Once this habit sticks, you’ll cleanly part ways with this exception.