Why LazyInitializationException Passes in Tests But Fails in Production
My tests passed perfectly. But production failed with LazyInitializationException. What happened?
I had an Order entity with lazy-loaded items:
@Entitypublic class Order { @Id private Long id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "order") private List<OrderItem> items; // Lazy-loaded collection
// getters, setters}In production, this code failed:
@Servicepublic class OrderService {
public OrderDto getOrder(Long id) { Order order = orderRepository.findById(id).orElseThrow(); // LazyInitializationException here in production! List<String> itemNames = order.getItems().stream() .map(OrderItem::getName) .collect(Collectors.toList()); return new OrderDto(order, itemNames); }}But my test passed without any exception:
@SpringBootTest@Transactional // <-- This is the culprit!class OrderServiceTest {
@Autowired private OrderService orderService;
@Test void shouldGetOrderWithItems() { // Test passes without LazyInitializationException OrderDto dto = orderService.getOrder(1L); assertThat(dto.items()).hasSize(3); // Works fine! }}Why did this happen?
The Root Cause: Session Management Differences
The problem is how Hibernate manages sessions in tests versus production.
How @Transactional Works in Tests
When I put @Transactional on my test class, Spring does this:
@SpringBootTest@Transactionalclass OrderServiceTest { // Tests run inside a transaction // Session stays OPEN for entire test method // Lazy loading works because Session is still active}Key behavior:
- Transaction starts before the test method
- Hibernate Session stays open throughout the test
- Lazy-loaded associations can be fetched on-demand
- Transaction rolls back after test completes
This means my lazy collection order.getItems() works fine because the Session is still active.
How It Works in Production
In production, without proper transaction management:
@Servicepublic class OrderService {
@Transactional(readOnly = true) public Order getOrder(Long id) { return orderRepository.findById(id).orElseThrow(); } // Transaction commits, Session CLOSES here}
// Later, outside transaction:Order order = orderService.getOrder(1L);order.getItems(); // LazyInitializationException!// Session is already closedThe problem:
- Transaction completes, Session closes
- Entity becomes detached
- Accessing lazy collection triggers initialization
- No Session available β LazyInitializationException
Visual: Session Lifecycle Comparison
TEST ENVIRONMENT:ββββββββββββββββββββββββββββββββββββββββββββββββββββ @Transactional Test Method ββ βββββββββββββββββββββββββββββββββββββββββββββ ββ β Session OPEN β ββ β βββββββββββ βββββββββββββββββββββββββββ β ββ β β Query β β Lazy Load Works Fine β β ββ β β Entity β β order.getItems() β β β ββ β βββββββββββ βββββββββββββββββββββββββββ β ββ β Session remains OPEN throughout β ββ βββββββββββββββββββββββββββββββββββββββββββββ ββ Transaction rolls back, Session closes ββββββββββββββββββββββββββββββββββββββββββββββββββββ
PRODUCTION ENVIRONMENT:ββββββββββββββββββββ βββββββββββββββββββββββββββββ Transactional β β Outside Transaction ββ Service Method β β ββ ββββββββββββββββ β β ββββββββββββββββββββββββ ββ β Session OPEN β β β β Session CLOSED β ββ β Query Entity β β β β Entity is DETACHED β ββ ββββββββββββββββ β β ββββββββββββββββββββββββ ββ Session CLOSES ββββββΆβ order.getItems() β ββ Entity DETACHED β β LazyInitializationEx! β βββββββββββββββββββββ ββββββββββββββββββββββββββββSolution 1: JOIN FETCH (Recommended for Read Operations)
When fetching entities for reading (not modifying), I use JOIN FETCH:
@Repositorypublic interface OrderRepository extends JpaRepository<Order, Long> {
// Single query fetches Order AND Items @Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id") Optional<Order> findByIdWithItems(@Param("id") Long id);}Then in my service:
@Servicepublic class OrderService {
public OrderDto getOrder(Long id) { Order order = orderRepository.findByIdWithItems(id).orElseThrow(); // Items already loaded, no lazy initialization needed return new OrderDto(order, order.getItems()); }}Important for pagination:
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.items", countQuery = "SELECT COUNT(o) FROM Order o")Page<Order> findAllWithItems(Pageable pageable);Solution 2: Entity Graph (Declarative Approach)
I can define a named entity graph on my entity:
@Entity@NamedEntityGraph( name = "Order.withItems", attributeNodes = @NamedAttributeNode("items"))public class Order { @Id private Long id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "order") private List<OrderItem> items;}Then use it in my repository:
@Repositorypublic interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(value = "Order.withItems") Optional<Order> findById(Long id);}Or ad-hoc in queries:
@EntityGraph(attributePaths = {"items", "customer"})List<Order> findByStatus(String status);Solution 3: DTO Projection (Best Performance)
To avoid entity detachment entirely, I use DTO projection:
public interface OrderSummary { Long getId(); String getCustomerName(); List<ItemSummary> getItems();
interface ItemSummary { String getName(); BigDecimal getPrice(); }}In my repository with constructor expression:
@Query("SELECT NEW com.example.OrderDto(o.id, o.customer.name, " + "(SELECT NEW com.example.ItemDto(i.name, i.price) FROM o.items i)) " + "FROM Order o WHERE o.id = :id")OrderDto findOrderDtoById(@Param("id") Long id);Benefits:
- No lazy loading issues
- Optimal query performance
- Immutable data transfer
- Clear API contract
Solution 4: Open Session in View (Anti-Pattern Warning)
In application.properties:
# Anti-pattern warning!spring.jpa.open-in-view=true # Default in Spring Boot 2.xThis keeps Hibernate Session open until view rendering completes. It allows lazy loading in controllers/views but is an anti-pattern.
Why itβs problematic:
- Performance: N+1 query problem not visible
- Resource leaks: Long-running database connections
- Testing gap: Same issue as @Transactional tests
- Transaction boundaries: Unclear responsibility
Recommendation: Disable it and fix the root cause:
spring.jpa.open-in-view=falseWriting Tests That Catch LazyInitializationException
Anti-Pattern: @Transactional on Test Class
// BAD: Masks the problem@SpringBootTest@Transactional // Session stays open, hiding the issueclass OrderServiceTest { @Test void shouldLoadOrderItems() { // This test passes but production will fail! Order order = service.getOrder(1L); assertThat(order.getItems()).isNotEmpty(); // Works in test }}Better Approach: Test Real Behavior
// GOOD: Tests actual production behavior@SpringBootTestclass OrderServiceTest {
@Autowired private OrderService orderService;
@Test void shouldThrowLazyInitializationException() { // This tests what actually happens in production Order order = orderService.getOrder(1L);
assertThrows(LazyInitializationException.class, () -> { order.getItems().size(); // Fails as expected }); }}Best Approach: Use @Transactional(propagation = NOT_SUPPORTED)
@SpringBootTest@Transactional(propagation = Propagation.NOT_SUPPORTED)class OrderServiceIntegrationTest {
@Test void shouldLoadOrderWithItemsUsingFetchJoin() { Order order = orderService.getOrderWithItems(1L); // Now testing the actual solution assertThat(order.getItems()).isNotEmpty(); // Works! }}Using @DataJpaTest Correctly
@DataJpaTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)class OrderRepositoryTest {
@Autowired private OrderRepository orderRepository;
@Test void shouldFetchOrderWithItems() { // Test repository method with JOIN FETCH Order order = orderRepository.findByIdWithItems(1L).orElseThrow();
// Access lazy collection outside transaction // Should work because items are already fetched assertThat(order.getItems()).hasSize(3); }}Decision Matrix: Which Solution to Choose?
| Solution | Use Case | Read/Write | Performance | Complexity |
|---|---|---|---|---|
| JOIN FETCH | Specific queries, known associations | Read | High | Low |
| Entity Graph | Declarative, reusable fetch plans | Read | High | Medium |
| DTO Projection | API responses, reporting | Read | Highest | Medium |
| @Transactional on service | Entity modifications | Write | Medium | Low |
Common Pitfalls and Solutions
Pitfall 1: N+1 Query Problem
Problem:
List<Order> orders = orderRepository.findAll();orders.forEach(o -> o.getItems().size()); // N+1 queries!Solution:
@Query("SELECT DISTINCT o FROM Order o LEFT JOIN FETCH o.items")List<Order> findAllWithItems();Pitfall 2: Multiple Bag Fetch
Problem:
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders LEFT JOIN FETCH u.addresses")// MultipleBagFetchException!Solution 1: Use Set instead of List:
@OneToManyprivate Set<Order> orders; // Instead of ListSolution 2: Two queries:
User user = userRepository.findByIdWithOrders(id);userRepository.findByIdWithAddresses(id); // Second queryPitfall 3: Testing with H2 vs Production Database
I use Testcontainers for real database testing:
@Testcontainers@SpringBootTestclass OrderServiceTest {
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); }}Summary and Best Practices
Checklist for Avoiding LazyInitializationException
- Never rely on
@Transactionalat test class level for lazy loading tests - Use JOIN FETCH for read operations
- Consider Entity Graph for reusable fetch strategies
- Prefer DTO projection for API responses
- Disable Open Session in View (
spring.jpa.open-in-view=false) - Use Testcontainers for database integration tests
- Test lazy loading behavior explicitly
Key Takeaways
@Transactionalon tests keeps Session open - This masks LazyInitializationException- Production closes Session after transaction - Causes the exception
- Use eager fetching strategies - JOIN FETCH, Entity Graph, DTOs
- Test real behavior - Not masked by test infrastructure
- Open Session in View is an anti-pattern - Disable it, fix the root cause
Complete Working Example
Entity:
@Entity@NamedEntityGraph(name = "Order.withItems", attributeNodes = @NamedAttributeNode("items"))public class Order { @Id @GeneratedValue private Long id;
@OneToMany(fetch = LAZY, mappedBy = "order") private List<OrderItem> items = new ArrayList<>();}Repository:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id") Optional<Order> findByIdWithItems(@Param("id") Long id);
@EntityGraph("Order.withItems") List<Order> findByStatus(String status);}Service:
@Servicepublic class OrderService {
@Transactional(readOnly = true) public Order getOrderWithItems(Long id) { return orderRepository.findByIdWithItems(id).orElseThrow(); }}Test:
@SpringBootTest@Transactional(propagation = Propagation.NOT_SUPPORTED)class OrderServiceTest {
@Autowired private OrderService orderService;
@Test void shouldLoadOrderWithItems() { Order order = orderService.getOrderWithItems(1L); assertThat(order.getItems()).isNotEmpty(); // No LazyInitializationException! }}Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- π¨βπ» Spring Boot JPA Documentation
- π¨βπ» Hibernate Documentation
Oh, and if you found these resources useful, donβt forget to support me by starring the repo on GitHub!
Comments