RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
HardSpring Boot#446 min readJul 11, 2026

Explain @EntityGraph.

performance
spring-data-jpa
jpa
hibernate
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this one because it reveals whether you can fix the N+1 query problem without turning every repository method into custom SQL.

Question: Explain @EntityGraph.

Answer: @EntityGraph is a Spring Data JPA annotation that tells JPA which related objects to fetch immediately for one repository query. Instead of loading an entity first and then triggering many lazy loads later, you ask for the needed associations up front. It changes the fetch plan, not the filter logic.

Interview-Ready Answer: “I use @EntityGraph when I want Spring Data JPA to fetch an entity plus specific relationships in one read operation. It helps me avoid N+1 queries and LazyInitializationException by defining a per-query fetch plan. Under the hood, Spring passes a JPA fetch graph or load graph hint to Hibernate, so I can keep my mappings simple and optimize only the repository method that needs it.”

🧠 Memory Map
Memory map — visual summary of this topic

What it is

@EntityGraph is a Spring Data JPA feature, not a core JPA annotation. A graph is just a list of attributes that should be initialized together with the main entity. This is useful when your entity mapping is intentionally lazy, but one read path needs more data right now.

How it works under the hood

  1. You put @EntityGraph on a repository method that returns entities, such as findById, a derived query, or a custom @Query method.
  2. Spring Data reads the annotation and adds a JPA query hint. In the default FETCH mode, it uses the jakarta.persistence.fetchgraph hint; in LOAD mode, it uses jakarta.persistence.loadgraph.
  3. Hibernate receives that hint and builds a fetch plan. A fetch plan means the loading strategy for that one query: which relationships must be ready immediately and which can stay lazy.
  4. The provider may use a join, a secondary select, or another optimized strategy. The exact SQL is provider-dependent, so the guarantee is about the loaded object graph, not one fixed SQL shape.
  5. When the repository method returns, the chosen associations are already initialized, so later access does not need another lazy hit.

Important detail: FETCH and LOAD are different. FETCH treats unspecified attributes as lazy for that query even if the entity mapping says otherwise. LOAD keeps the mapping defaults and only upgrades the listed paths.

When and why to use it

  • Use it for read-heavy screens that need a parent plus a few children, like order details, user profiles, or admin dashboards.
  • Use it when you want to kill N+1 without writing verbose JPQL everywhere.
  • Use it when you want repository-level control, so one method stays lean and another method can fetch more.

Comparison with common alternatives

OptionMeaningMain trade-off
@EntityGraphPer-query fetch planClean and reusable
JOIN FETCHJPQL forces a joinVery explicit, less reusable
Default LAZYLoad on accessSimple, but N+1 risk

Performance and edge cases

Think in query count, not just syntax. If you load 20 orders and each order touches 5 line items, naive lazy access can become 21 queries or more. With an entity graph, that often becomes one fixed fetch plan, or a small number of SQL statements, depending on the provider. The downside is memory and row inflation: if you fetch a parent with a large collection, the SQL result set can grow fast, and multiple collection joins can multiply rows.

A good interview answer also mentions limits. @EntityGraph does not help DTO projections because DTOs bypass entity loading. It also does not magically make every query faster; a graph that is too big can be slower than a few well-batched lazy loads. If you have many small collections, batching with @BatchSize or hibernate.default_batch_fetch_size may be a better fit.

Memory model: @EntityGraph is like handing the database a shopping list before it walks into the warehouse: “Pick these shelves now, don’t make me come back for them one by one.”

Real-world story

Imagine a checkout service in an e-commerce app. The order page needs the order header, the line items, and maybe the customer summary. A developer leaves the mapping lazy and then serializes the order in the controller. In production, the page becomes slow because each order row triggers extra database hits, and sometimes it fails with LazyInitializationException when the session is already closed.

The symptoms are easy to spot: p95 latency climbs from around 80 ms to 700 ms or more, the database CPU spikes, and logs show repeated select ... from order_items where purchase_order_id = ?. Users see spinning loaders, and support tickets say the order page is “randomly broken.” The fix is often to put @EntityGraph(attributePaths = "items") on the repository method that loads the order detail view, so the page gets one consistent fetch plan instead of accidental lazy loading.

Spring Boot
package com.example.entitygraph;

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.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
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.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.repository.query.Param;

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

@SpringBootApplication
@EntityScan(basePackageClasses = {PurchaseOrder.class, OrderItem.class})
@EnableJpaRepositories(basePackageClasses = PurchaseOrderRepository.class)
public class EntityGraphDemoApplication {

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

    @Bean
    CommandLineRunner demo(PurchaseOrderRepository repo) {
        return args -> {
            // Save a parent plus children. Cascade makes the example small and realistic.
            PurchaseOrder order = new PurchaseOrder("ORD-1001");
            order.addItem(new OrderItem("Keyboard", 1));
            order.addItem(new OrderItem("Mouse", 2));
            PurchaseOrder saved = repo.save(order);

            Long id = saved.getId();
            System.out.println("Saved order id = " + id);

            // Plain lookup returns a detached entity after the repository call ends.
            // Accessing a LAZY collection here demonstrates the classic failure path.
            PurchaseOrder plain = repo.findById(id).orElseThrow();
            try {
                System.out.println("Plain fetch item count = " + plain.getItems().size());
            } catch (Exception ex) {
                System.out.println("Expected lazy-loading failure: " + ex.getClass().getSimpleName());
                System.out.println(ex.getMessage());
            }

            // The entity graph tells Hibernate to fetch items with this query.
            // That means the collection is already initialized when the method returns.
            PurchaseOrder graphLoaded = repo.findWithItemsById(id).orElseThrow();
            System.out.println("EntityGraph fetch item count = " + graphLoaded.getItems().size());
            for (OrderItem item : graphLoaded.getItems()) {
                System.out.println(item.getProductName() + " x" + item.getQuantity());
            }
        };
    }
}

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

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

    private String orderNumber;

    @OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();

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

    PurchaseOrder(String orderNumber) {
        this.orderNumber = orderNumber;
    }

    void addItem(OrderItem item) {
        items.add(item);
        item.setPurchaseOrder(this);
    }

    public Long getId() {
        return id;
    }

    public String getOrderNumber() {
        return orderNumber;
    }

    public List<OrderItem> getItems() {
        return items;
    }
}

@Entity
@Table(name = "order_items")
class OrderItem {

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

    private String productName;
    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "purchase_order_id")
    private PurchaseOrder purchaseOrder;

    protected OrderItem() {
        // JPA needs a no-args constructor.
    }

    OrderItem(String productName, int quantity) {
        this.productName = productName;
        this.quantity = quantity;
    }

    void setPurchaseOrder(PurchaseOrder purchaseOrder) {
        this.purchaseOrder = purchaseOrder;
    }

    public String getProductName() {
        return productName;
    }

    public int getQuantity() {
        return quantity;
    }
}

interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {

    // A custom query plus @EntityGraph is a common Spring Data pattern.
    // The graph changes how the entity is fetched; the JPQL still selects the same row.
    @EntityGraph(attributePaths = "items")
    @Query("select p from PurchaseOrder p where p.id = :id")
    Optional<PurchaseOrder> findWithItemsById(@Param("id") Long id);
}

Follow-up & Tricky Questions:

  • How is FETCH different from LOAD?
    FETCH treats unspecified attributes as lazy for that query, even if the mapping says eager. LOAD keeps the mapping defaults and only adds the paths from the graph.
  • Can I use @EntityGraph with findAll(Pageable)?
    Yes, if the repository method returns entities. Just be careful with large collections because pagination and collection fetching can still create heavy SQL or duplicate root rows depending on the provider.
  • Does @EntityGraph always produce one SQL statement?
    No. It is a fetch hint, not a promise about SQL shape. Hibernate may use a join or multiple selects; the goal is to initialize the requested graph efficiently.
  • When should I use JOIN FETCH instead?
    Use JOIN FETCH when you want one very explicit JPQL query and you are comfortable owning that query text. Use @EntityGraph when you want the fetch plan attached to the repository method and keep JPQL simpler.
  • Can it fetch nested paths like items.product?
    Yes, you can list nested attribute paths, but each extra hop increases the chance of a large result set. Test the SQL and row count, not just the annotation.
  • Does it work with DTO projections?
    No. Entity graphs apply to entity loading, while DTO projections skip entity materialization and therefore ignore the graph.
  • Can I fetch two collections at once?
    Technically yes, but it is a classic trap because joining multiple collections can explode the number of rows. In many systems, it is better to fetch one collection and batch or separately load the others.

Common Mistakes:

  • Thinking it filters rows. It does not change what data matches the query; it only changes what gets initialized with the result.
  • Assuming one fixed SQL shape. The provider may join or issue secondary selects, so explain the behavior, not a guessed query.
  • Using it on DTO queries. Graphs are for entity loading; DTO projections bypass them.
  • Making the graph too large. A giant graph can be slower and heavier than a few well-batched lazy loads.

Memory Hook: Think of @EntityGraph as giving the warehouse a shopping list before delivery: “Pick these shelves now, not one trip at a time.”

Cheat Sheet:

  • Spring Data JPA annotation for per-query fetching.
  • Used to avoid N+1 queries and lazy-loading surprises.
  • FETCH is the default; LOAD preserves mapping defaults.
  • Works on entity-returning repository methods, including custom @Query methods.
  • Great for detail pages and read-heavy endpoints; test carefully with large collections.

Practice Tasks:

  • Add @EntityGraph to a user repository to load roles with the user.
  • Remove it and watch the extra lazy queries in logs, then compare timings.
  • Try a second graph with a nested path like orders.items and inspect the SQL size.
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.entitygraph; 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.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.Bean; 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.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.repository.query.Param; import java.util.ArrayList; import java.util.List; import java.util.Optional; @SpringBootApplication @EntityScan(basePackageClasses = {PurchaseOrder.class, OrderItem.class}) @EnableJpaRepositories(basePackageClasses = PurchaseOrderRepository.class) public class EntityGraphDemoApplication { public static void main(String[] args) { SpringApplication.run(EntityGraphDemoApplication.class, args); } @Bean CommandLineRunner demo(PurchaseOrderRepository repo) { return args -> { // Save a parent plus children. Cascade makes the example small and realistic. PurchaseOrder order = new PurchaseOrder("ORD-1001"); order.addItem(new OrderItem("Keyboard", 1)); order.addItem(new OrderItem("Mouse", 2)); PurchaseOrder saved = repo.save(order); Long id = saved.getId(); System.out.println("Saved order id = " + id); // Plain lookup returns a detached entity after the repository call ends. // Accessing a LAZY collection here demonstrates the classic failure path. PurchaseOrder plain = repo.findById(id).orElseThrow(); try { System.out.println("Plain fetch item count = " + plain.getItems().size()); } catch (Exception ex) { System.out.println("Expected lazy-loading failure: " + ex.getClass().getSimpleName()); System.out.println(ex.getMessage()); } // The entity graph tells Hibernate to fetch items with this query. // That means the collection is already initialized when the method returns. PurchaseOrder graphLoaded = repo.findWithItemsById(id).orElseThrow(); System.out.println("EntityGraph fetch item count = " + graphLoaded.getItems().size()); for (OrderItem item : graphLoaded.getItems()) { System.out.println(item.getProductName() + " x" + item.getQuantity()); } }; } } @Entity @Table(name = "purchase_orders") class PurchaseOrder { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String orderNumber; @OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List<OrderItem> items = new ArrayList<>(); protected PurchaseOrder() { // JPA needs a no-args constructor. } PurchaseOrder(String orderNumber) { this.orderNumber = orderNumber; } void addItem(OrderItem item) { items.add(item); item.setPurchaseOrder(this); } public Long getId() { return id; } public String getOrderNumber() { return orderNumber; } public List<OrderItem> getItems() { return items; } } @Entity @Table(name = "order_items") class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String productName; private int quantity; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "purchase_order_id") private PurchaseOrder purchaseOrder; protected OrderItem() { // JPA needs a no-args constructor. } OrderItem(String productName, int quantity) { this.productName = productName; this.quantity = quantity; } void setPurchaseOrder(PurchaseOrder purchaseOrder) { this.purchaseOrder = purchaseOrder; } public String getProductName() { return productName; } public int getQuantity() { return quantity; } } interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> { // A custom query plus @EntityGraph is a common Spring Data pattern. // The graph changes how the entity is fetched; the JPQL still selects the same row. @EntityGraph(attributePaths = "items") @Query("select p from PurchaseOrder p where p.id = :id") Optional<PurchaseOrder> findWithItemsById(@Param("id") Long id); }