RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
TrickySpring Boot#428 min readJul 11, 2026

Lazy Loading vs Eager Loading.

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this question because one fetch choice can make the same endpoint either fast or painfully slow.

Question: What is lazy loading vs eager loading in Spring Data JPA?

Answer: Lazy loading means JPA postpones the database call until you actually touch the relationship. Eager loading means JPA fetches the relationship immediately when the parent entity is loaded. In Spring Boot, lazy is usually safer for collections and data you may never use, while eager is only a good fit for small relations that are always needed.

Interview-Ready Answer: “In Spring Data JPA, lazy loading delays the database hit until I access the relationship, while eager loading fetches it right away with the parent. I usually prefer lazy for collections because it avoids loading data I may never need, but I watch out for LazyInitializationException if I access it after the transaction ends. One important JPA detail is that @ManyToOne and @OneToOne are eager by default, while collection mappings like @OneToMany are lazy by default.”

🧠 Memory Map
Memory map — visual summary of this topic

What it really means

Detailed Explanation: Think of a relation as a folder on a desk. Lazy loading keeps the folder closed and only opens it when you ask for the contents. Eager loading opens the folder immediately and puts everything on the desk, even if you never look at it. In JPA, a proxy is a stand-in object that acts like the real entity until Hibernate needs the real data, and the persistence context is the active unit of work where Hibernate can still talk to the database safely.

How it works under the hood

  1. Spring Data JPA asks Hibernate to load the parent entity, such as Customer.
  2. For a lazy relation, Hibernate does not load the child rows immediately; it places a proxy or collection wrapper into the parent object.
  3. While the EntityManager is open and the transaction is active, the first access to that relation triggers a SQL query for the missing data.
  4. If that access happens after the session is closed, Hibernate cannot fetch anything anymore, so you get LazyInitializationException.
  5. For eager loading, Hibernate loads the child data right away, usually with a join or a follow-up select, so the object graph is complete sooner but heavier in memory.

When to use each one

  • Use lazy for collections like orders, line items, comments, or tags because those sets can grow large and are often not needed on every request.
  • Use eager only for tiny, always-needed data where the extra fetch cost is predictable, such as a small reference object.
  • Prefer query-time fetching with JOIN FETCH or @EntityGraph when one screen needs extra data but other screens do not. That gives you control without changing the entity mapping globally.

Lazy vs eager comparison

StrategyWhen SQL runsMain winMain risk
LazyOn first accessLess data upfrontLazy exception, N+1
EagerImmediatelySimple object graphToo much data, heavy joins

That table is the interview core: lazy saves work now, eager spends work now. The hidden trap is that eager loading is not automatically “better”; if you load 100 customers with a 20-row child collection, you can multiply rows and memory very quickly. A typical service with a pool of 10 database connections can still become slow if each request fires 1 parent query plus 20 child queries, because the bottleneck becomes round trips, not raw CPU.

Performance and complexity notes

  • Lazy initial load: usually one parent query, so the first cost is close to O(1) SQL statements for the parent.
  • Lazy access pattern: each relation access is another query, so across a loop it can become O(N) queries and lead to the classic N+1 problem, meaning one query for the list plus one query per row.
  • Eager loading: often one wider query or a small number of queries, but the result set may be much larger, which increases memory use and can duplicate rows because of joins.
  • Mitigation: if lazy causes too many queries, use fetch joins, @EntityGraph, or batch fetching such as hibernate.default_batch_fetch_size instead of switching the whole mapping to eager.

Important edge cases

  • JSON serialization can trigger lazy loading unexpectedly when a controller returns an entity directly.
  • toString(), logging, or debugging watches can also touch lazy fields and fire SQL at the wrong time.
  • @ManyToOne is eager by default in JPA, which surprises many people because they expect every relation to behave like a collection.
  • @Basic(fetch = FetchType.LAZY) is only a hint for simple fields and is not something to rely on casually.
  • Spring Boot can hide lazy problems when the view layer keeps the session open, so many teams turn that off to fail fast and keep fetches explicit.

Real-World Story: Imagine a checkout service in an e-commerce app. The Order endpoint returns an order summary, and each order has a collection of OrderItem rows. The developer marks the items as lazy, which is fine inside the service layer, but the controller returns the entity directly and Jackson tries to serialize the items after the transaction is already closed. In staging, everything looked fine because the session stayed open longer; in production, requests start failing with 500s and logs show failed to lazily initialize a collection of role. Users see a spinning checkout page, and support gets tickets about missing order details. The opposite bug is just as bad: someone makes the items eager, and every homepage request starts joining huge child tables, pushing latency from 120 ms to nearly a second.

Spring Boot
// File: src/main/java/com/example/lazyloadingdemo/LazyLoadingDemoApplication.java
package com.example.lazyloadingdemo;

import org.hibernate.LazyInitializationException;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class LazyLoadingDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LazyLoadingDemoApplication.class, args);
    }

    @Bean
    CommandLineRunner demo(CustomerRepository customerRepository, CustomerService customerService) {
        return args -> {
            Customer alice = new Customer("Alice");
            alice.addOrder(new PurchaseOrder("Keyboard"));
            alice.addOrder(new PurchaseOrder("Mouse"));
            Customer saved = customerRepository.save(alice);
            Long id = saved.getId();

            System.out.println("\n=== 1) LAZY works inside a transaction ===");
            int countInsideTx = customerService.countOrdersInsideTransaction(id);
            System.out.println("Orders counted safely inside tx: " + countInsideTx);

            System.out.println("\n=== 2) LAZY fails after the session is closed ===");
            Customer detached = customerService.loadDetachedCustomer(id);
            try {
                // The collection is still a proxy here; accessing it now forces a DB hit.
                System.out.println("Detached order count: " + detached.getOrders().size());
            } catch (LazyInitializationException ex) {
                System.out.println("Expected failure: " + ex.getClass().getSimpleName());
                System.out.println("Message: " + ex.getMessage());
            }

            System.out.println("\n=== 3) Eager-style fetch for one query using EntityGraph ===");
            Customer withOrders = customerService.loadCustomerWithOrders(id);
            System.out.println("Orders loaded with the query: " + withOrders.getOrders().size());
        };
    }
}

// File: src/main/java/com/example/lazyloadingdemo/CustomerService.java
package com.example.lazyloadingdemo;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CustomerService {

    private final CustomerRepository customerRepository;

    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Transactional(readOnly = true)
    public int countOrdersInsideTransaction(Long id) {
        Customer customer = customerRepository.findById(id).orElseThrow();
        // Safe here because the transaction is still open.
        return customer.getOrders().size();
    }

    @Transactional(readOnly = true)
    public Customer loadDetachedCustomer(Long id) {
        // Returning the entity ends the transaction when the method returns.
        // The caller gets a detached object, which is perfect for demonstrating the lazy failure.
        return customerRepository.findById(id).orElseThrow();
    }

    @Transactional(readOnly = true)
    public Customer loadCustomerWithOrders(Long id) {
        // This does not change the mapping globally; it fetches eagerly only for this query.
        return customerRepository.loadByIdWithOrders(id).orElseThrow();
    }
}

// File: src/main/java/com/example/lazyloadingdemo/CustomerRepository.java
package com.example.lazyloadingdemo;

import java.util.Optional;

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface CustomerRepository extends JpaRepository<Customer, Long> {

    @EntityGraph(attributePaths = "orders")
    @Query("select c from Customer c where c.id = :id")
    Optional<Customer> loadByIdWithOrders(@Param("id") Long id);
}

// File: src/main/java/com/example/lazyloadingdemo/Customer.java
package com.example.lazyloadingdemo;

import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // Collections are usually lazy so we do not load every child row on every request.
    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<PurchaseOrder> orders = new ArrayList<>();

    protected Customer() {
        // JPA needs a no-arg constructor.
    }

    public Customer(String name) {
        this.name = name;
    }

    public void addOrder(PurchaseOrder order) {
        orders.add(order);
        order.setCustomer(this);
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<PurchaseOrder> getOrders() {
        return orders;
    }
}

// File: src/main/java/com/example/lazyloadingdemo/PurchaseOrder.java
package com.example.lazyloadingdemo;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

@Entity
@Table(name = "purchase_orders")
public class PurchaseOrder {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String productName;

    // Many-to-one is EAGER by default in JPA, but LAZY is usually the safer choice for APIs.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;

    protected PurchaseOrder() {
        // JPA needs a no-arg constructor.
    }

    public PurchaseOrder(String productName) {
        this.productName = productName;
    }

    void setCustomer(Customer customer) {
        this.customer = customer;
    }

    public Long getId() {
        return id;
    }

    public String getProductName() {
        return productName;
    }
}

// File: src/main/resources/application.properties
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.open-in-view=false

Follow-up & Tricky Questions:

  • Why not make everything eager? Because eager loading can explode the amount of data pulled into memory and can create very wide joins. It looks simple at the entity level but often becomes slow and expensive in real APIs.
  • How do you safely load lazy data in a REST API? Fetch it inside a transactional service method, or use JOIN FETCH / @EntityGraph for the exact query that needs it. Do not rely on the controller layer to magically fix fetch timing.
  • What is the N+1 problem? It means one query loads the parent list and then one more query runs for each row or relation access. The code looks innocent, but a loop over 50 rows can become 51 SQL statements.
  • What is the difference between JOIN FETCH and @EntityGraph? Both are ways to fetch associated data eagerly for one query. JOIN FETCH is written in JPQL, while @EntityGraph is metadata you attach to the repository method.
  • Does Spring Boot change JPA fetch defaults? No, Spring Boot does not rewrite the JPA defaults; Hibernate follows the mapping you declare. Boot mainly affects surrounding behavior such as transaction boundaries and whether the web layer keeps the session open.
  • Is lazy loading the same as async loading? No. Lazy loading is still synchronous; it just waits until you access the field. The SQL still runs on the same thread and inside the same request unless the session is already closed.
  • If I mark a field lazy, will Hibernate never query it? No. The query happens the moment the field is accessed, serialized, logged, or otherwise touched. Lazy means deferred, not disabled.
  • Does eager loading always mean one SQL query? Not necessarily. Hibernate may use joins or extra selects depending on the mapping and query shape, so eager means load now, not one magical query every time.
  • Can basic fields be lazy too? Sometimes, but that is a special case and usually depends on bytecode enhancement and provider support. For interviews, focus first on associations, because that is where the real production problems happen.

Common Mistakes:

  • Making every association eager. This often causes bigger queries, more memory use, and slower APIs. Correction: keep collections lazy and fetch eagerly only for the exact use case that needs it.
  • Returning JPA entities straight from controllers. Serialization may trigger lazy loading outside a transaction. Correction: map to DTOs or fetch the data fully inside the service layer.
  • Ignoring the default fetch types. Many candidates remember collection defaults but forget that @ManyToOne and @OneToOne are eager by default. Correction: state the defaults clearly in the interview.
  • Using eager loading to hide an N+1 problem. That may remove one symptom but create a much heavier query. Correction: fix the query shape with fetch joins, entity graphs, or batch fetching.

Memory Hook: Lazy is “bring the file only when I open the drawer”; eager is “dump the files on the desk before the meeting starts.”

Cheat Sheet:

  • Lazy = load on first access.
  • Eager = load immediately with the parent.
  • JPA defaults: @ManyToOne and @OneToOne are eager; collections are usually lazy.
  • Lazy saves work upfront, but can throw LazyInitializationException outside a transaction.
  • Eager avoids later surprises, but can overfetch and slow down large object graphs.
  • For one screen only, prefer JOIN FETCH or @EntityGraph over changing the global mapping.

Practice Tasks:

  • Change the demo to make PurchaseOrder.customer eager and observe the SQL difference.
  • Add a second collection to Customer and see how the row count changes with eager fetching.
  • Turn spring.jpa.open-in-view on and off in a web app, then compare when the lazy exception appears.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

// File: src/main/java/com/example/lazyloadingdemo/LazyLoadingDemoApplication.java package com.example.lazyloadingdemo; import org.hibernate.LazyInitializationException; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class LazyLoadingDemoApplication { public static void main(String[] args) { SpringApplication.run(LazyLoadingDemoApplication.class, args); } @Bean CommandLineRunner demo(CustomerRepository customerRepository, CustomerService customerService) { return args -> { Customer alice = new Customer("Alice"); alice.addOrder(new PurchaseOrder("Keyboard")); alice.addOrder(new PurchaseOrder("Mouse")); Customer saved = customerRepository.save(alice); Long id = saved.getId(); System.out.println("\n=== 1) LAZY works inside a transaction ==="); int countInsideTx = customerService.countOrdersInsideTransaction(id); System.out.println("Orders counted safely inside tx: " + countInsideTx); System.out.println("\n=== 2) LAZY fails after the session is closed ==="); Customer detached = customerService.loadDetachedCustomer(id); try { // The collection is still a proxy here; accessing it now forces a DB hit. System.out.println("Detached order count: " + detached.getOrders().size()); } catch (LazyInitializationException ex) { System.out.println("Expected failure: " + ex.getClass().getSimpleName()); System.out.println("Message: " + ex.getMessage()); } System.out.println("\n=== 3) Eager-style fetch for one query using EntityGraph ==="); Customer withOrders = customerService.loadCustomerWithOrders(id); System.out.println("Orders loaded with the query: " + withOrders.getOrders().size()); }; } } // File: src/main/java/com/example/lazyloadingdemo/CustomerService.java package com.example.lazyloadingdemo; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class CustomerService { private final CustomerRepository customerRepository; public CustomerService(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } @Transactional(readOnly = true) public int countOrdersInsideTransaction(Long id) { Customer customer = customerRepository.findById(id).orElseThrow(); // Safe here because the transaction is still open. return customer.getOrders().size(); } @Transactional(readOnly = true) public Customer loadDetachedCustomer(Long id) { // Returning the entity ends the transaction when the method returns. // The caller gets a detached object, which is perfect for demonstrating the lazy failure. return customerRepository.findById(id).orElseThrow(); } @Transactional(readOnly = true) public Customer loadCustomerWithOrders(Long id) { // This does not change the mapping globally; it fetches eagerly only for this query. return customerRepository.loadByIdWithOrders(id).orElseThrow(); } } // File: src/main/java/com/example/lazyloadingdemo/CustomerRepository.java package com.example.lazyloadingdemo; import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface CustomerRepository extends JpaRepository<Customer, Long> { @EntityGraph(attributePaths = "orders") @Query("select c from Customer c where c.id = :id") Optional<Customer> loadByIdWithOrders(@Param("id") Long id); } // File: src/main/java/com/example/lazyloadingdemo/Customer.java package com.example.lazyloadingdemo; import java.util.ArrayList; import java.util.List; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // Collections are usually lazy so we do not load every child row on every request. @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List<PurchaseOrder> orders = new ArrayList<>(); protected Customer() { // JPA needs a no-arg constructor. } public Customer(String name) { this.name = name; } public void addOrder(PurchaseOrder order) { orders.add(order); order.setCustomer(this); } public Long getId() { return id; } public String getName() { return name; } public List<PurchaseOrder> getOrders() { return orders; } } // File: src/main/java/com/example/lazyloadingdemo/PurchaseOrder.java package com.example.lazyloadingdemo; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @Entity @Table(name = "purchase_orders") public class PurchaseOrder { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String productName; // Many-to-one is EAGER by default in JPA, but LAZY is usually the safer choice for APIs. @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "customer_id") private Customer customer; protected PurchaseOrder() { // JPA needs a no-arg constructor. } public PurchaseOrder(String productName) { this.productName = productName; } void setCustomer(Customer customer) { this.customer = customer; } public Long getId() { return id; } public String getProductName() { return productName; } } // File: src/main/resources/application.properties spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.open-in-view=false