Choosing between EAGER and LAZY loading in Hibernate is one of the most impactful performance decisions in a Spring Boot app. Learn when each is appropriate and what the tradeoffs are.
JOptimize Team
The EAGER vs LAZY loading decision in Hibernate is one of the most consequential performance choices in a Spring Boot application. Get it wrong and you either load gigabytes of data you don't need, or your app crashes with LazyInitializationException at runtime. Most developers discover the right approach through painful production incidents rather than upfront understanding.
Hibernate has different defaults depending on the association type:
@Entity public class Order { @ManyToOne // Default: EAGER private Customer customer; @OneToMany // Default: LAZY private List<OrderItem> items; @ManyToMany // Default: LAZY private List<Tag> tags; @OneToOne // Default: EAGER private ShippingAddress shippingAddress; }
@ManyToOne and @OneToOne default to EAGER - every time you load an Order, Hibernate automatically loads the Customer and ShippingAddress. This is usually wrong for performance.
// @ManyToOne(fetch = FetchType.EAGER) - the default @Entity public class Order { @ManyToOne // EAGER private Customer customer; @OneToOne // EAGER private ShippingAddress shippingAddress; @ManyToOne // EAGER private Warehouse warehouse; } // This "simple" query actually loads Order + Customer + ShippingAddress + Warehouse List<Order> orders = orderRepository.findAll(); // If orders has 1000 rows, and each eager load fires a query: // 1 + 1000 + 1000 + 1000 = 3001 queries - an N+1 problem you didn't even see coming
EAGER loading creates hidden N+1 problems. You didn't write JOIN FETCH - Hibernate does it automatically for every query on that entity, whether you need the related data or not.
@Entity public class Order { @OneToMany(fetch = FetchType.LAZY) // Won't load until accessed private List<OrderItem> items; } @Service public class OrderService { @Transactional public Order getOrder(Long id) { return orderRepository.findById(id).orElseThrow(); } // ? Transaction closes here } @RestController public class OrderController { public OrderResponse getOrder(Long id) { Order order = orderService.getOrder(id); // Transaction is already closed! order.getItems().size(); // ? LazyInitializationException! } }
Lazy collections are only loadable while the Hibernate session is open. Once the transaction closes, accessing an uninitialized lazy collection throws LazyInitializationException.
Always override @ManyToOne and @OneToOne to LAZY:
@Entity public class Order { @ManyToOne(fetch = FetchType.LAZY) // ? Override default private Customer customer; @OneToOne(fetch = FetchType.LAZY) // ? Override default private ShippingAddress shippingAddress; @OneToMany // ? Already LAZY by default private List<OrderItem> items; }
When you need related data, load it explicitly for that specific query:
@Repository public interface OrderRepository extends JpaRepository<Order, Long> { // For the order detail page - needs customer and items @Query("SELECT o FROM Order o JOIN FETCH o.customer JOIN FETCH o.items WHERE o.id = :id") Optional<Order> findByIdWithDetails(@Param("id") Long id); // For the order list - only needs customer name @EntityGraph(attributePaths = {"customer"}) Page<Order> findAll(Pageable pageable); // For order processing - needs warehouse @EntityGraph(attributePaths = {"warehouse"}) List<Order> findByStatusAndWarehouseRegion(OrderStatus status, String region); }
For read-only endpoints, load only the columns you need with JPQL constructor expressions:
@Query("SELECT new com.example.dto.OrderSummary(o.id, o.status, c.name, o.total) " + "FROM Order o JOIN o.customer c WHERE o.customerId = :customerId") List<OrderSummary> findSummariesByCustomer(@Param("customerId") Long customerId);
No lazy loading issues - the DTO is fully constructed in the query.
spring.jpa.open-in-view=true (Spring Boot's default) keeps the session open for the entire HTTP request, masking LazyInitializationException. This is a known anti-pattern:
# Disable Open Session in View (recommended) spring.jpa.open-in-view=false
With it disabled, you're forced to be explicit about what you load - which leads to better performance. Fix existing LazyInitializationException errors with JOIN FETCH or DTOs as described above.
Sometimes you need to force-load a lazy collection within an existing transaction:
@Transactional public Order getOrderWithItems(Long id) { Order order = orderRepository.findById(id).orElseThrow(); Hibernate.initialize(order.getItems()); // Forces load within transaction return order; }
Use this sparingly - it's better to use JOIN FETCH in the query than to initialize after the fact.
FetchType.EAGER on collections - Hibernate will load the entire collection every time the parent is loaded, regardless of contextsize() on a lazy collection just to check if it's empty - use isEmpty() or a COUNT query instead to avoid loading the entire collectionThe correct Hibernate fetch strategy is: everything LAZY by default, load eagerly only for specific queries using JOIN FETCH or @EntityGraph. Override @ManyToOne and @OneToOne defaults to LAZY. Disable open-in-view to surface loading issues early. Use DTOs for read-heavy endpoints to avoid loading entities altogether.
JOptimize scans your Spring Boot entities for EAGER collections and @ManyToOne associations missing explicit fetch = LAZY, and flags them with a 1-click auto-fix.
Fix EAGER loading anti-patterns before they cause N+1 queries in production - free scan, no configuration required.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.