Skip to content

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:

Order.java
@Entity
public 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:

OrderService.java
@Service
public 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:

OrderServiceTest.java
@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:

OrderServiceTest.java
@SpringBootTest
@Transactional
class OrderServiceTest {
// Tests run inside a transaction
// Session stays OPEN for entire test method
// Lazy loading works because Session is still active
}

Key behavior:

  1. Transaction starts before the test method
  2. Hibernate Session stays open throughout the test
  3. Lazy-loaded associations can be fetched on-demand
  4. 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:

OrderService.java
@Service
public 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 closed

The problem:

  1. Transaction completes, Session closes
  2. Entity becomes detached
  3. Accessing lazy collection triggers initialization
  4. No Session available β†’ LazyInitializationException

Visual: Session Lifecycle Comparison

Session Lifecycle
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! β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When fetching entities for reading (not modifying), I use JOIN FETCH:

OrderRepository.java
@Repository
public interface OrderRepository extends JpaRepository&lt;Order, Long&gt; {
// Single query fetches Order AND Items
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id")
Optional&lt;Order&gt; findByIdWithItems(@Param("id") Long id);
}

Then in my service:

OrderService.java
@Service
public 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:

OrderRepository.java with pagination
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.items",
countQuery = "SELECT COUNT(o) FROM Order o")
Page&lt;Order&gt; findAllWithItems(Pageable pageable);

Solution 2: Entity Graph (Declarative Approach)

I can define a named entity graph on my entity:

Order.java with Entity Graph
@Entity
@NamedEntityGraph(
name = "Order.withItems",
attributeNodes = @NamedAttributeNode("items")
)
public class Order {
@Id
private Long id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "order")
private List&lt;OrderItem&gt; items;
}

Then use it in my repository:

OrderRepository.java with Entity Graph
@Repository
public interface OrderRepository extends JpaRepository&lt;Order, Long&gt; {
@EntityGraph(value = "Order.withItems")
Optional&lt;Order&gt; findById(Long id);
}

Or ad-hoc in queries:

Ad-hoc Entity Graph
@EntityGraph(attributePaths = {"items", "customer"})
List&lt;Order&gt; findByStatus(String status);

Solution 3: DTO Projection (Best Performance)

To avoid entity detachment entirely, I use DTO projection:

OrderSummary.java
public interface OrderSummary {
Long getId();
String getCustomerName();
List&lt;ItemSummary&gt; getItems();
interface ItemSummary {
String getName();
BigDecimal getPrice();
}
}

In my repository with constructor expression:

OrderRepository.java with DTO
@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:

application.properties
# Anti-pattern warning!
spring.jpa.open-in-view=true # Default in Spring Boot 2.x

This keeps Hibernate Session open until view rendering completes. It allows lazy loading in controllers/views but is an anti-pattern.

Why it’s problematic:

  1. Performance: N+1 query problem not visible
  2. Resource leaks: Long-running database connections
  3. Testing gap: Same issue as @Transactional tests
  4. Transaction boundaries: Unclear responsibility

Recommendation: Disable it and fix the root cause:

application.properties
spring.jpa.open-in-view=false

Writing Tests That Catch LazyInitializationException

Anti-Pattern: @Transactional on Test Class

BAD Test Example
// BAD: Masks the problem
@SpringBootTest
@Transactional // Session stays open, hiding the issue
class 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 Test Example
// GOOD: Tests actual production behavior
@SpringBootTest
class 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)

Best Test Example
@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

Repository Test Example
@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?

SolutionUse CaseRead/WritePerformanceComplexity
JOIN FETCHSpecific queries, known associationsReadHighLow
Entity GraphDeclarative, reusable fetch plansReadHighMedium
DTO ProjectionAPI responses, reportingReadHighestMedium
@Transactional on serviceEntity modificationsWriteMediumLow

Common Pitfalls and Solutions

Pitfall 1: N+1 Query Problem

Problem:

N+1 Problem Example
List&lt;Order&gt; orders = orderRepository.findAll();
orders.forEach(o -> o.getItems().size()); // N+1 queries!

Solution:

N+1 Solution
@Query("SELECT DISTINCT o FROM Order o LEFT JOIN FETCH o.items")
List&lt;Order&gt; findAllWithItems();

Pitfall 2: Multiple Bag Fetch

Problem:

MultipleBagFetchException
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders LEFT JOIN FETCH u.addresses")
// MultipleBagFetchException!

Solution 1: Use Set instead of List:

Use Set Instead of List
@OneToMany
private Set&lt;Order&gt; orders; // Instead of List

Solution 2: Two queries:

Two Queries Solution
User user = userRepository.findByIdWithOrders(id);
userRepository.findByIdWithAddresses(id); // Second query

Pitfall 3: Testing with H2 vs Production Database

I use Testcontainers for real database testing:

Testcontainers Example
@Testcontainers
@SpringBootTest
class OrderServiceTest {
@Container
static PostgreSQLContainer&lt;?&gt; postgres = new PostgreSQLContainer&lt;&gt;("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 @Transactional at 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

  1. @Transactional on tests keeps Session open - This masks LazyInitializationException
  2. Production closes Session after transaction - Causes the exception
  3. Use eager fetching strategies - JOIN FETCH, Entity Graph, DTOs
  4. Test real behavior - Not masked by test infrastructure
  5. Open Session in View is an anti-pattern - Disable it, fix the root cause

Complete Working Example

Entity:

Order.java Complete Example
@Entity
@NamedEntityGraph(name = "Order.withItems", attributeNodes = @NamedAttributeNode("items"))
public class Order {
@Id @GeneratedValue
private Long id;
@OneToMany(fetch = LAZY, mappedBy = "order")
private List&lt;OrderItem&gt; items = new ArrayList&lt;&gt;();
}

Repository:

OrderRepository.java Complete Example
public interface OrderRepository extends JpaRepository&lt;Order, Long&gt; {
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id")
Optional&lt;Order&gt; findByIdWithItems(@Param("id") Long id);
@EntityGraph("Order.withItems")
List&lt;Order&gt; findByStatus(String status);
}

Service:

OrderService.java Complete Example
@Service
public class OrderService {
@Transactional(readOnly = true)
public Order getOrderWithItems(Long id) {
return orderRepository.findByIdWithItems(id).orElseThrow();
}
}

Test:

OrderServiceTest.java Complete Example
@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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments