RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#436 min readJul 11, 2026

Explain N+1 Query Problem.

practice
learning
Practice modeTest yourself instead of reading straight through

Why interviewers ask this: it shows whether you can spot a hidden database bottleneck before it becomes a production slowdown.

Question: Explain the N+1 query problem in Spring Boot with Spring Data JPA.

Answer: The N+1 query problem happens when your app loads one list of parent rows with one SQL query, then runs one extra query for each parent row when you touch a lazy relationship such as a collection. So if I load 50 authors and then read each author’s books, I may execute 51 queries instead of 1. In Spring Data JPA, this usually appears with `@OneToMany` or `@ManyToOne` mappings and is fixed by changing the fetch plan, not by adding more Java code.

Interview-Ready Answer: I’d say the N+1 query problem is when I fetch one set of entities with one query, but then Hibernate fires one additional query per entity as soon as I access a lazy association. In Spring Boot, I usually solve it with a fetch join, an `@EntityGraph`, or batch fetching, depending on whether I need pagination and how much data I’m loading. The key point is that the cost is not the loop in Java — it is the extra database round trips.

🧠 Memory Map
Memory map — visual summary of this topic

What it means

Detailed Explanation: N+1 is query amplification: one query to load the parents, then N more queries to load children one parent at a time. Hibernate does this most often with lazy loading, where a relation is fetched only when you touch it.

Under the hood

  1. Spring Data JPA loads the parent entities, for example `SELECT * FROM author`.
  2. Your code loops over the results and calls something like `author.getBooks()`.
  3. Hibernate checks the persistence context, which is the first-level cache for the current session, to see whether the collection is already loaded.
  4. If the collection is not loaded, Hibernate issues another SQL query for that one author, such as `SELECT * FROM book WHERE author_id = ?`.
  5. The loop repeats, so the number of child queries grows with the number of parents. If there are 100 authors, you can easily get 101 queries.

How to fix it

ApproachQueriesGood forCaveat
Lazy access + loop1 + NVery small dataHidden round trips
Fetch joinUsually 1Read-heavy screensDuplicates, pagination issues
`@EntityGraph`1 or a fewDeclarative fetch plansProvider behavior matters
Batch fetching1 + ceil(N/B)Large listsStill more than one query

For many Spring Boot apps, the best default is to keep relations lazy and fetch them explicitly on the query that needs them. A fetch join is often the cleanest option when you know exactly which graph you need. If the list is large or pagination matters, batch fetching with a size like 16 or 32 can reduce the damage without exploding the result set.

Performance notes

The time cost is mostly network and database round trips, so the problem grows with latency. A page with 80 parents can become 81 queries; at only 5 ms of extra round-trip time each, that is roughly 400 ms of extra delay before you count database work. Query count is the big-O story here: naive access is O(N) queries, while batch fetching becomes roughly O(N / B), where B is the batch size. Space use also rises because Hibernate must keep more managed entities in memory, and fetch joins can duplicate parent rows in the SQL result set.

Important edge cases

  • `@OneToMany` is lazy by default, but `@ManyToOne` is eager by default in JPA; eager does not guarantee one SQL statement.
  • Using fetch join on a collection with pagination can give wrong page sizes because SQL paginates rows, not distinct parent objects.
  • Joining multiple `List` collections can trigger Hibernate’s `MultipleBagFetchException`.
  • Spring Boot web apps often run with open-in-view enabled by default, which can hide lazy-loading failures during view rendering, but it does not remove N+1.

Memory model: think of it as one shopping cart and N extra warehouse trips: the cart load is cheap, but every time you reach for a new item, the warehouse gets another call.

Real-World Example: In an e-commerce admin service, an `Order` page showed each order with its line items. A developer added `order.getItems().size()` inside a loop to display counts, and the endpoint went from a few SQL statements to hundreds. In production, the symptom was slow page loads, a noisy log full of repeated `select ... where order_id = ?` statements, and connection pool pressure during peak traffic. Users saw timeouts, and the fix was to replace the naive loop with a fetch join for that screen plus a DTO so the controller only received the fields it needed.

Spring Boot
package com.example.nplusone;

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

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 NPlusOneDemoApplication {

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

    @Bean
    CommandLineRunner runner(AuthorRepository authorRepository, DemoService demoService) {
        return args -> {
            authorRepository.deleteAll();
            seed(authorRepository);

            demoService.showLazyInitializationFailure();
            demoService.showNPlusOne();
            demoService.showFetchJoin();
        };
    }

    private void seed(AuthorRepository repo) {
        Author alice = new Author("Alice");
        alice.addBook(new Book("Spring in Action"));
        alice.addBook(new Book("JPA Mastery"));

        Author bob = new Author("Bob");
        bob.addBook(new Book("Hibernate Tips"));

        Author cara = new Author("Cara"); // Edge case: a parent with no children still participates in the query.

        repo.saveAll(List.of(alice, bob, cara));
    }
}

// File: src/main/java/com/example/nplusone/Author.java
package com.example.nplusone;

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;

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

    private String name;

    // Lazy keeps the initial query small; touching the collection later can trigger the N in N+1.
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<Book> books = new ArrayList<>();

    protected Author() {
    }

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

    public void addBook(Book book) {
        books.add(book);
        book.setAuthor(this);
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Book> getBooks() {
        return books;
    }
}

// File: src/main/java/com/example/nplusone/Book.java
package com.example.nplusone;

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;

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

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;

    protected Book() {
    }

    public Book(String title) {
        this.title = title;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }
}

// File: src/main/java/com/example/nplusone/AuthorRepository.java
package com.example.nplusone;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface AuthorRepository extends JpaRepository<Author, Long> {

    // DISTINCT removes duplicate Author objects caused by joining to many Book rows.
    @Query("select distinct a from Author a left join fetch a.books")
    List<Author> findAllWithBooks();
}

// File: src/main/java/com/example/nplusone/DemoService.java
package com.example.nplusone;

import java.util.List;

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

@Service
public class DemoService {
    private final AuthorRepository authorRepository;

    public DemoService(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    public void showLazyInitializationFailure() {
        System.out.println("\n--- LazyInitializationException demo ---");
        List<Author> authors = authorRepository.findAll();
        try {
            // Outside a transaction, the lazy collection has no open session to load itself.
            System.out.println("First author's books: " + authors.get(0).getBooks().size());
        } catch (LazyInitializationException ex) {
            System.out.println("Expected failure: " + ex.getClass().getSimpleName() + " - " + ex.getMessage());
        }
    }

    @Transactional(readOnly = true)
    public void showNPlusOne() {
        System.out.println("\n--- N+1 demo ---");
        List<Author> authors = authorRepository.findAll();

        int totalBooks = 0;
        for (Author author : authors) {
            // Each access can trigger one SELECT per author when the collection is still lazy.
            int count = author.getBooks().size();
            totalBooks += count;
            System.out.println(author.getName() + " -> " + count + " books");
        }
        System.out.println("Total books = " + totalBooks);
    }

    @Transactional(readOnly = true)
    public void showFetchJoin() {
        System.out.println("\n--- Fetch join demo ---");
        List<Author> authors = authorRepository.findAllWithBooks();

        int totalBooks = 0;
        for (Author author : authors) {
            // Books are already loaded by the join, so this loop does not fan out into extra queries.
            int count = author.getBooks().size();
            totalBooks += count;
            System.out.println(author.getName() + " -> " + count + " books");
        }
        System.out.println("Total books = " + totalBooks);
    }
}

// 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
logging.level.org.hibernate.SQL=DEBUG

Follow-up & Tricky Questions:

  • How do you fix N+1 in Spring Data JPA? Use a fetch join for the exact screen, an `@EntityGraph` for a reusable fetch plan, or batch fetching when a join would be too large.
  • Why not just make everything eager? Eager loading can still produce many queries or huge joins, so it shifts the problem instead of solving it; fetch only what the use case needs.
  • What is the difference between fetch join and `@EntityGraph`? A fetch join is written in JPQL and is very explicit; `@EntityGraph` keeps the repository method cleaner and lets Spring Data JPA apply the fetch plan declaratively.
  • How do you detect N+1 early? Turn on SQL logging in dev, watch for repeated `where ... = ?` statements in loops, and test with realistic data sizes instead of one happy-path row.
  • When would you use DTO projections instead of entities? When the screen only needs a subset of fields, a DTO avoids loading unnecessary relationships and often prevents accidental lazy traversal.
  • Can N+1 happen on `ManyToOne` too? Yes. Even though `ManyToOne` is eager by default in JPA, loading many child rows can still result in repeated lookups or oversized joins depending on the provider and query shape.
  • Is `open-in-view` a fix? No. It only keeps the session open longer so lazy loading works in the web layer; it can hide the symptom, but the extra queries still happen.
  • Does `distinct` in JPQL remove duplicate SQL rows? Not necessarily; it mainly deduplicates root entities in Hibernate memory, while the database may still return repeated joined rows.

Common Mistakes:

  • Using eager loading everywhere. Correction: eager is not a universal fix; choose the fetch plan per use case.
  • Confusing N+1 with LazyInitializationException. Correction: lazy init is a session-lifetime problem; N+1 is a query-count problem. They can appear together, but they are different.
  • Fetching too much with join fetch. Correction: a big join can blow up memory and break pagination, so use DTOs or batch fetching when the graph is large.
  • Not checking SQL logs. Correction: always verify the actual SQL, because the loop in Java often looks harmless while the database is doing the real work.

Memory Hook: One shopping cart, many warehouse trips. Load the cart once, but every extra lazy lookup is another trip to the warehouse.

Cheat Sheet:

  • N+1 means 1 parent query + N child queries.
  • The trigger is usually lazy access inside a loop.
  • Best fixes: fetch join, `@EntityGraph`, or batch fetching.
  • `@OneToMany` is lazy by default; `@ManyToOne` is eager by default.
  • Watch pagination, duplicates, and `MultipleBagFetchException`.
  • SQL logs tell the truth; Java loops do not.

Practice Tasks:

  • Add a `Customer` and `Order` model, then reproduce N+1 by reading `order.getCustomer().getName()` in a loop.
  • Replace the naive query with a fetch join and compare the SQL logs.
  • Try a paginated endpoint and observe why fetch joining a collection can become awkward.
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

package com.example.nplusone; import java.util.ArrayList; import java.util.List; 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 NPlusOneDemoApplication { public static void main(String[] args) { SpringApplication.run(NPlusOneDemoApplication.class, args); } @Bean CommandLineRunner runner(AuthorRepository authorRepository, DemoService demoService) { return args -> { authorRepository.deleteAll(); seed(authorRepository); demoService.showLazyInitializationFailure(); demoService.showNPlusOne(); demoService.showFetchJoin(); }; } private void seed(AuthorRepository repo) { Author alice = new Author("Alice"); alice.addBook(new Book("Spring in Action")); alice.addBook(new Book("JPA Mastery")); Author bob = new Author("Bob"); bob.addBook(new Book("Hibernate Tips")); Author cara = new Author("Cara"); // Edge case: a parent with no children still participates in the query. repo.saveAll(List.of(alice, bob, cara)); } } // File: src/main/java/com/example/nplusone/Author.java package com.example.nplusone; 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; @Entity public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // Lazy keeps the initial query small; touching the collection later can trigger the N in N+1. @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List<Book> books = new ArrayList<>(); protected Author() { } public Author(String name) { this.name = name; } public void addBook(Book book) { books.add(book); book.setAuthor(this); } public Long getId() { return id; } public String getName() { return name; } public List<Book> getBooks() { return books; } } // File: src/main/java/com/example/nplusone/Book.java package com.example.nplusone; 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; @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "author_id") private Author author; protected Book() { } public Book(String title) { this.title = title; } public Long getId() { return id; } public String getTitle() { return title; } public Author getAuthor() { return author; } public void setAuthor(Author author) { this.author = author; } } // File: src/main/java/com/example/nplusone/AuthorRepository.java package com.example.nplusone; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; public interface AuthorRepository extends JpaRepository<Author, Long> { // DISTINCT removes duplicate Author objects caused by joining to many Book rows. @Query("select distinct a from Author a left join fetch a.books") List<Author> findAllWithBooks(); } // File: src/main/java/com/example/nplusone/DemoService.java package com.example.nplusone; import java.util.List; import org.hibernate.LazyInitializationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class DemoService { private final AuthorRepository authorRepository; public DemoService(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } public void showLazyInitializationFailure() { System.out.println("\n--- LazyInitializationException demo ---"); List<Author> authors = authorRepository.findAll(); try { // Outside a transaction, the lazy collection has no open session to load itself. System.out.println("First author's books: " + authors.get(0).getBooks().size()); } catch (LazyInitializationException ex) { System.out.println("Expected failure: " + ex.getClass().getSimpleName() + " - " + ex.getMessage()); } } @Transactional(readOnly = true) public void showNPlusOne() { System.out.println("\n--- N+1 demo ---"); List<Author> authors = authorRepository.findAll(); int totalBooks = 0; for (Author author : authors) { // Each access can trigger one SELECT per author when the collection is still lazy. int count = author.getBooks().size(); totalBooks += count; System.out.println(author.getName() + " -> " + count + " books"); } System.out.println("Total books = " + totalBooks); } @Transactional(readOnly = true) public void showFetchJoin() { System.out.println("\n--- Fetch join demo ---"); List<Author> authors = authorRepository.findAllWithBooks(); int totalBooks = 0; for (Author author : authors) { // Books are already loaded by the join, so this loop does not fan out into extra queries. int count = author.getBooks().size(); totalBooks += count; System.out.println(author.getName() + " -> " + count + " books"); } System.out.println("Total books = " + totalBooks); } } // 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 logging.level.org.hibernate.SQL=DEBUG