You fetch a user from the database, everything works fine, then suddenly—boom. LazyInitializationException: failed to lazily initialize a collection of role: User.orders, could not initialize proxy - no Session. This is probably the most confusing error for Spring Boot developers new to JPA.

TLDR - Quick Fix

LazyInitializationException happens when you try to access a lazy-loaded relationship after the Hibernate session closes. Here’s the fastest fix:

// ❌ PROBLEM: Session closes before accessing orders
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    User user = userRepository.findById(id).orElseThrow();
    return user; // Later, user.getOrders() throws LazyInitializationException
}

// ✅ FIX: Fetch the data within the transaction
@GetMapping("/users/{id}")
@Transactional(readOnly = true)
public UserDTO getUser(@PathVariable Long id) {
    User user = userRepository.findById(id).orElseThrow();
    user.getOrders().size(); // Force initialization while session is open
    return new UserDTO(user); // Return DTO instead of entity
}

Quick diagnostic checklist:

  1. Are you accessing a relationship marked fetch = FetchType.LAZY?
  2. Is the access happening outside a @Transactional method?
  3. Are you returning entities directly from the controller?
  4. Check if you’re accessing relationships in JSP/Thymeleaf templates

Let’s walk through why this happens and the best ways to fix it for different scenarios.

Understanding Lazy Loading in JPA

When you define entity relationships in JPA, they can be loaded eagerly or lazily:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    // By default, @OneToMany and @ManyToMany are LAZY
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;

    // By default, @ManyToOne and @OneToOne are EAGER
    @ManyToOne(fetch = FetchType.EAGER)
    private Company company;
}

Lazy loading means Hibernate doesn’t fetch the orders until you explicitly call user.getOrders(). This saves database queries when you don’t need that data.

The catch? Hibernate needs an open session (database connection) to fetch that lazy data. Once the session closes, trying to access orders throws LazyInitializationException.

Here’s the lifecycle:

// 1. Repository method starts (session opens)
@Transactional
public User findUser(Long id) {
    User user = userRepository.findById(id).orElseThrow();
    // Session is still open - can access lazy relationships
    user.getOrders().size(); // ✅ Works
    return user;
} // 2. Method ends (session closes)

// 3. Controller tries to use the user
@GetMapping("/users/{id}")
public String showUser(@PathVariable Long id) {
    User user = userService.findUser(id);
    // Session is CLOSED now
    user.getOrders(); // ❌ LazyInitializationException!
}

The Complete Error Message

When it happens, you’ll see something like:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role:
com.example.model.User.orders, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize
    at org.hibernate.collection.internal.AbstractPersistentCollection.read
    at org.hibernate.collection.internal.PersistentBag.iterator
    ...

The key parts: “failed to lazily initialize” + “no Session” = you tried to access lazy data after the database connection closed.

Solution #1: Use @Transactional (Simplest)

The easiest fix is making sure you access the data within a transactional boundary.

Before:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    // No @Transactional - session closes immediately after query
    public User getUser(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        User user = userService.getUser(id);
        user.getOrders(); // ❌ Session already closed
        return user;
    }
}

After:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = true) // Session stays open for entire method
    public User getUserWithOrders(Long id) {
        User user = userRepository.findById(id).orElseThrow();
        user.getOrders().size(); // Trigger initialization
        return user;
    }
}

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUserWithOrders(id); // ✅ Orders already loaded
    }
}

Calling user.getOrders().size() forces Hibernate to execute the query while the session is still open. Any subsequent access to orders works because the collection is now initialized.

Pro tip: Use readOnly = true for read operations—it’s a performance hint to Hibernate.

Solution #2: Use DTOs (Best Practice)

Returning entities directly from controllers is actually an anti-pattern. It couples your API to your database schema and can expose sensitive data. Using DTOs (Data Transfer Objects) solves both problems.

// Create a DTO
public class UserDTO {
    private Long id;
    private String username;
    private List<OrderDTO> orders;

    public UserDTO(User user) {
        this.id = user.getId();
        this.username = user.getUsername();
        // Access lazy collection during conversion (while session is open)
        this.orders = user.getOrders().stream()
            .map(OrderDTO::new)
            .collect(Collectors.toList());
    }

    // Getters
}

public class OrderDTO {
    private Long id;
    private String productName;
    private BigDecimal price;

    public OrderDTO(Order order) {
        this.id = order.getId();
        this.productName = order.getProductName();
        this.price = order.getPrice();
    }

    // Getters
}

Service layer:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = true)
    public UserDTO getUserWithOrders(Long id) {
        User user = userRepository.findById(id).orElseThrow();
        return new UserDTO(user); // Conversion happens inside transaction
    }
}

Controller:

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users/{id}")
    public UserDTO getUser(@PathVariable Long id) {
        return userService.getUserWithOrders(id); // ✅ Returns DTO, not entity
    }
}

This approach gives you:

  • No lazy loading issues (data is copied during DTO creation)
  • API independence from database schema
  • Control over what data is exposed
  • Easier versioning of your API

Solution #3: Custom Repository Queries with Fetch Joins

When you know you’ll need the related data, fetch it eagerly using JPQL or query methods.

Using JPQL:

public interface UserRepository extends JpaRepository<User, Long> {

    // Custom query with JOIN FETCH
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@PathVariable Long id);

    // You can also fetch multiple relationships
    @Query("SELECT u FROM User u " +
           "LEFT JOIN FETCH u.orders " +
           "LEFT JOIN FETCH u.company " +
           "WHERE u.id = :id")
    Optional<User> findByIdWithOrdersAndCompany(@PathVariable Long id);
}

Using Entity Graphs (alternative approach):

public interface UserRepository extends JpaRepository<User, Long> {

    @EntityGraph(attributePaths = {"orders", "company"})
    Optional<User> findById(Long id);
}

In your service:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    // No @Transactional needed - data is already fetched
    public User getUserWithOrders(Long id) {
        return userRepository.findByIdWithOrders(id).orElseThrow();
    }
}

The JOIN FETCH tells Hibernate to load orders in the same query as User. No second query needed, no lazy loading issue.

When to use this approach:

  • You ALWAYS need the related data
  • You’re worried about the N+1 query problem
  • You want fewer database round trips

Solution #4: Change to Eager Fetching (Use with Caution)

You can make the relationship eager by default:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Order> orders; // Now always fetched
}

Why this is usually a bad idea:

  • Every time you load a User, you ALWAYS load orders, even if you don’t need them
  • Can lead to performance issues (fetching lots of unnecessary data)
  • Can cause cartesian products with multiple eager relationships

When it’s okay:

  • The relationship is small (e.g., user settings, profile)
  • You legitimately need it 90%+ of the time
  • The related entity has very few rows

Most of the time, lazy loading with explicit fetching (solution #3) is better.

Solution #5: Enable Open-Session-In-View (Not Recommended)

Spring Boot has a feature called “Open Session in View” that keeps the Hibernate session open for the entire HTTP request, even in the controller and view layers.

In application.properties:

# This is actually the DEFAULT in Spring Boot
spring.jpa.open-in-view=true

When enabled, you can access lazy relationships anywhere during request processing:

@GetMapping("/users/{id}")
public String showUser(@PathVariable Long id, Model model) {
    User user = userRepository.findById(id).orElseThrow();
    // OSIV keeps session open, so this works
    model.addAttribute("orderCount", user.getOrders().size());
    return "userDetails"; // Template can also access lazy relationships
}

Why you should disable it:

spring.jpa.open-in-view=false

Reasons:

  1. Hidden performance issues: You might trigger dozens of lazy queries in your view layer without realizing it (N+1 problem)
  2. Database connections held longer: The connection stays open during JSON serialization, template rendering, etc.
  3. Unclear transaction boundaries: Makes it hard to reason about when queries happen
  4. Production bottlenecks: Can exhaust connection pools under load

It’s better to be explicit about when data is fetched using the other solutions.

Common Scenarios and Fixes

Scenario 1: JSON serialization in REST APIs

// Problem: Jackson tries to serialize lazy collections
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id).orElseThrow();
    // Jackson calls getOrders() -> LazyInitializationException
}

// Fix: Use @JsonIgnore or DTOs
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonIgnore // Don't serialize this field
    private List<Order> orders;
}

// Better: Use DTOs (see Solution #2)

Scenario 2: Thymeleaf templates

// Problem: Template accesses lazy relationship
@GetMapping("/users/{id}")
public String showUser(@PathVariable Long id, Model model) {
    User user = userRepository.findById(id).orElseThrow();
    model.addAttribute("user", user);
    return "userDetails"; // Template tries to loop over user.orders
}

// Fix: Fetch the data beforehand
@GetMapping("/users/{id}")
@Transactional(readOnly = true)
public String showUser(@PathVariable Long id, Model model) {
    User user = userRepository.findById(id).orElseThrow();
    user.getOrders().size(); // Initialize
    model.addAttribute("user", user);
    return "userDetails";
}

Scenario 3: Async methods and scheduled tasks

// Problem: @Async method runs outside original transaction
@Service
public class NotificationService {

    @Async
    public void sendOrderConfirmation(User user) {
        // New thread, no session
        user.getOrders(); // ❌ LazyInitializationException
    }
}

// Fix: Pass only IDs or DTOs to async methods
@Service
public class NotificationService {

    @Autowired
    private UserRepository userRepository;

    @Async
    @Transactional(readOnly = true)
    public void sendOrderConfirmation(Long userId) {
        User user = userRepository.findByIdWithOrders(userId).orElseThrow();
        user.getOrders(); // ✅ Fetched in this transaction
    }
}

Debugging Tips

1. Enable SQL logging to see what’s happening:

# application.properties
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

This shows you exactly when queries run and helps identify N+1 problems.

2. Use Hibernate.isInitialized() to check if a collection is loaded:

import org.hibernate.Hibernate;

if (Hibernate.isInitialized(user.getOrders())) {
    // Safe to access
    user.getOrders().size();
} else {
    // Would throw LazyInitializationException if accessed
    logger.warn("Orders not initialized for user {}", user.getId());
}

3. Add breakpoints with session info:

When debugging, check if a session is available:

EntityManager em; // Inject this
boolean sessionOpen = em.getEntityManagerFactory()
    .getPersistenceUnitUtil()
    .isLoaded(user, "orders");

Best Practices Summary

Here’s my recommended approach based on years of Spring Boot development:

  1. Default to lazy loading - Don’t change relationships to eager unless you have a good reason
  2. Use DTOs in controllers - Never return entities from REST endpoints
  3. Fetch what you need explicitly - Use JOIN FETCH or @EntityGraph when you know you need related data
  4. Disable OSIV - Set spring.jpa.open-in-view=false and handle lazy loading explicitly
  5. Keep transactions short - Don’t hold database connections longer than necessary
  6. Think about your queries - Use SQL logging during development to catch N+1 problems early

Here’s the pattern I use in most projects:

// Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@PathVariable Long id);
}

// Service
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = true)
    public UserDTO getUserWithOrders(Long id) {
        User user = userRepository.findByIdWithOrders(id).orElseThrow();
        return new UserDTO(user);
    }
}

// Controller
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users/{id}/orders")
    public UserDTO getUserOrders(@PathVariable Long id) {
        return userService.getUserWithOrders(id);
    }
}

This keeps your code clean, performant, and free from lazy loading issues.

When you do hit a LazyInitializationException, the stack trace can be massive and confusing. Use Debugly’s trace formatter to quickly parse and analyze Java stack traces. It highlights the entity and relationship causing the problem, helping you identify whether it’s happening in a controller, service, or template. The formatted output makes it easy to pinpoint exactly where the session closed and where you tried to access the lazy data.

For more Spring troubleshooting, check out our guide on Spring Boot BeanCreationException, another common Spring Boot error.

Now go fix those lazy loading issues!