Spring BootSpring Data Jpaintermediate
Updated:

Spring Boot Spring Data JPA Interview Questions and Answers

9 min read

Spring Data JPA interview questions for Spring Boot — repository hierarchy, derived queries, @Query, N+1, LAZY vs EAGER, @Transactional, @Version and Pageable, answered properly.

TL;DR – Quick Answer

Spring Data JPA gives you repository interfaces that generate data-access code from method names and JPQL, over JPA/Hibernate. Interviews test the repository hierarchy, derived query methods, @Query with JPQL and native SQL, the N+1 problem and its fixes, LAZY vs EAGER loading, @Transactional semantics, optimistic locking with @Version, and pagination. The recurring trap is the persistence context — knowing when entities are managed and when queries actually fire.

On This Page

Spring Data JPA is where most Spring Boot services meet their database, and interviewers use it to test two things at once: whether you can use the repository abstraction fluently, and whether you understand the JPA machinery underneath it — the persistence context, lazy loading, transactions and the N+1 trap. The candidates who stand out are the ones who know when a query actually fires and when an entity is managed, because that is where the subtle production bugs live.

Explain the Spring Data JPA repository hierarchy.

At the base is Repository (a marker), then CrudRepository (basic CRUD), PagingAndSortingRepository (adds paging and sorting), and JpaRepository, which extends those and adds JPA-specific batch and flush operations plus List-returning methods. You declare an interface extending JpaRepository<Entity, IdType> and Spring generates the implementation at runtime.

The point of the hierarchy is capability layering — you extend the level whose API you need. Most applications extend JpaRepository for the fullest surface (findAll, saveAll, flush, saveAndFlush, paging). You never write the implementation; Spring Data creates a proxy backed by SimpleJpaRepository at startup.

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCustomerIdAndStatus(Long customerId, OrderStatus status);
}

Interview note: Trap: "who implements your repository interface?" Spring Data JPA generates a proxy at startup delegating to SimpleJpaRepository. Candidates who think they must write an @Repository class for basic CRUD miss the entire premise of Spring Data.

How do derived query methods work?

Spring Data parses the method name into a query. Keywords like findBy, And, Or, GreaterThan, Between, Like, OrderBy map to a JPQL WHERE clause and ordering, so findByStatusAndTotalGreaterThan(status, amount) becomes a query without you writing any JPQL.

It's convenient for straightforward finders but degrades fast — findByCustomerIdAndStatusAndCreatedAtBetweenOrderByCreatedAtDesc is unreadable and brittle. The senior instinct is to switch to @Query once a derived name gets long, because an explicit query is clearer and easier to optimize. Derived methods are validated at startup against the entity's fields, so a typo in a property name fails fast rather than at runtime.

Interview note: Follow-up: "what happens if you misspell a field in a derived method name?" The application fails to start — Spring Data validates the parsed property path against the entity metamodel during context initialization. That fail-fast behavior is a feature, not a nuisance.

When do you use @Query, and what's the difference between JPQL and native queries?

Use @Query when a derived method would be awkward or when you need something JPQL can't express. JPQL queries operate on entities and fields (SELECT o FROM Order o WHERE o.status = :status) and are database-agnostic; native queries (nativeQuery = true) are raw SQL against tables and columns, for database-specific features or complex SQL.

JPQL is the default choice because it's portable and works in terms of your object model, so refactoring a column name is contained. Native queries buy you vendor features (window functions, hints, specific SQL) at the cost of portability and of bypassing some JPA conveniences. Named parameters (:status) keep queries readable and safe from injection.

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT o FROM Order o WHERE o.status = :status AND o.total > :min")
    List<Order> findActiveAbove(@Param("status") OrderStatus status,
                                @Param("min") BigDecimal min);

    @Query(value = "SELECT * FROM orders WHERE created_at > now() - interval '7 days'",
           nativeQuery = true)
    List<Order> findRecentNative();
}

Interview note: Trap: "does a native query return managed entities?" If it selects all columns of a mapped entity and you declare the entity return type, yes — they're managed. But projections and partial selects return non-managed data, so changes to them aren't persisted. Knowing this distinction avoids a "why didn't my update save" surprise.

What is @Modifying, and why is it needed?

@Modifying marks a @Query as an UPDATE or DELETE (a write) rather than a SELECT, so Spring Data executes it as a bulk update instead of trying to read results. It must run inside a transaction, and because it operates directly on the database, it bypasses the persistence context.

That bypass is the catch: a bulk @Modifying update changes rows in the database but doesn't update entities already loaded in the current persistence context, which can then be stale. You often pair it with clearAutomatically = true (or flush/clear) so the context doesn't serve outdated entities after the bulk operation.

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Order o SET o.status = :status WHERE o.createdAt < :cutoff")
int expireOldOrders(@Param("status") OrderStatus status, @Param("cutoff") Instant cutoff);

Interview note: Follow-up: "why does the method return an int?" A bulk update returns the count of affected rows. That count is often exactly what you want (how many orders expired), and it's a signal the query ran as a bulk operation, not entity-by-entity.

What is the N+1 problem and how do you fix it?

N+1 happens when you load N parent entities, then Hibernate fires one additional query per parent to load a lazy association — 1 + N queries where 1 or 2 would do. Fix it with a fetch join (JOIN FETCH) in @Query, an @EntityGraph, or batch fetching, so the children load together.

It's the single most common JPA performance bug, and interviewers love it because the fix reveals whether you understand lazy loading. A findAll() over 100 orders, each iterating order.getItems(), silently issues 101 queries. A fetch join collapses that to one. @EntityGraph does the same declaratively on a repository method without hand-writing the join.

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
    List<Order> findWithItems(@Param("status") OrderStatus status);

    @EntityGraph(attributePaths = "items")
    List<Order> findByStatus(OrderStatus status);
}

Interview note: Trap: "you added JOIN FETCH on two collections and got a MultipleBagFetchException or a cartesian explosion — why?" Fetching two collection associations in one query multiplies rows cartesian-style. Fetch one collection per query, use Set instead of List where appropriate, or split into separate queries / batch fetching. This nuance separates people who've hit N+1 in production from those who've only read about it.

LAZY vs EAGER — what's the difference and what do you default to?

LAZY loads an association only when you first access it; EAGER loads it immediately with the parent. @OneToMany/@ManyToMany default to LAZY, and @ManyToOne/@OneToOne default to EAGER. The strong recommendation is to make associations LAZY and fetch what you need explicitly per query.

EAGER is seductive because it "just works" without LazyInitializationException, but it means every load of the parent drags the association along whether you need it or not — and multiple EAGER associations compound into large joins and their own N+1 patterns. LAZY plus targeted fetch joins gives you control: load the graph the specific use case needs, nothing more.

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

    @ManyToOne(fetch = FetchType.LAZY)   // override the EAGER default
    private Customer customer;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();
}

Note the jakarta.persistence.* imports in Spring Boot 3 — @Entity, @Id, @ManyToOne all come from jakarta.persistence, not javax.persistence.

Interview note: Follow-up: "if LAZY is the recommendation, how do you avoid LazyInitializationException?" Fetch the needed associations inside the transaction (fetch join / entity graph), or map to a DTO in the query. The fix is loading the right data in the service layer — not switching to EAGER, which trades one problem for a performance one.

Explain @Transactional semantics, propagation and readOnly.

@Transactional wraps a method in a database transaction via a proxy: it commits on normal return and rolls back on unchecked exceptions by default. propagation controls how it behaves relative to an existing transaction (REQUIRED joins one or starts one, REQUIRES_NEW suspends and starts a separate one). readOnly = true signals no writes, letting Hibernate skip dirty checking and flushing.

The persistence context lives for the length of the transaction, which is why the boundary matters so much for JPA: inside it, entities are managed and lazy loading works; once it closes, they're detached. readOnly on query methods is a cheap, honest optimization that also communicates intent. Propagation is where trade-offs get interesting — REQUIRES_NEW borrows a second connection, which affects pool sizing.

@Service
public class OrderService {

    @Transactional(readOnly = true)
    public OrderDto view(Long id) {                 // no dirty checking, no flush
        return mapper.toDto(orderRepo.findWithItems(id));
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void auditIndependently(AuditEvent e) {  // commits even if caller rolls back
        auditRepo.save(e);
    }
}

Interview note: Trap: "your @Transactional method calls another @Transactional method in the same class and the propagation is ignored — why?" Because @Transactional works through a proxy, and a self-invocation (this.method()) never crosses it, so the annotation on the inner call is skipped. Split into separate beans or inject the proxy.

save() vs saveAndFlush(), and how does the entity lifecycle work?

save() makes an entity managed and schedules the INSERT/UPDATE, but the SQL may not hit the database until the transaction flushes (commit, a query that needs current state, or an explicit flush). saveAndFlush() forces the flush immediately. Entities move through states: transient (new, unmanaged) → managed (in the persistence context) → detached (context closed) → removed.

Understanding flush timing explains a lot of "why didn't I see my insert" confusion. Within a transaction, changes to a managed entity are written on flush without an explicit save() at all — that's automatic dirty checking. saveAndFlush() is for when you need the generated ID or a constraint check right now, before the transaction ends.

Interview note: Follow-up: "do you need to call save() on a managed entity you modified?" No — if the entity is managed and you change a field inside the transaction, dirty checking flushes the UPDATE automatically. Calling save() on an already-managed entity is redundant, though harmless.

How does optimistic locking with @Version work?

Add a @Version field (an int/long or timestamp) to the entity. On each update Hibernate includes the current version in the WHERE clause and increments it; if another transaction changed the row first, zero rows match and Hibernate throws OptimisticLockException. It prevents lost updates without holding database locks.

Optimistic locking suits the common case where conflicts are rare: no lock is held during the user's think-time, and the conflict is detected only at write. The alternative, pessimistic locking (@Lock(LockModeType.PESSIMISTIC_WRITE)), holds a real database lock and is for high-contention rows where you'd rather block than retry.

@Entity
public class Account {
    @Id private Long id;
    private BigDecimal balance;

    @Version
    private long version;   // jakarta.persistence.Version
}

Interview note: Trap: "you get OptimisticLockException — what's the correct response?" Not to suppress it. Catch it and retry the read-modify-write, or surface a "the record changed, please review" message. The exception is the system correctly stopping a lost update, so the fix is a retry or user prompt, not disabling versioning.

What interviewers really test

Spring Data JPA questions probe the gap between using the repository abstraction and understanding what it does — the persistence context, when queries fire, when entities are managed, and where the N+1 and LazyInitializationException traps hide. Anyone can extend JpaRepository; the signal is whether you can explain why a bulk update left a stale entity, or why a fetch join fixed 101 queries.

Build the foundation through the Spring Boot learning path, then connect JPA to its neighbors: it sits directly behind the web layer, so pair it with Spring MVC, and the transaction and proxy behavior that trips people up is really a container topic, which the bean scopes questions reinforce. Once you can trace a request through a transactional service to a fetch-joined query and back to JSON — without a stray N+1 or a detached-entity surprise — a Spring Data JPA mock interview becomes a demonstration rather than an interrogation.

Frequently Asked Questions

What is the difference between CrudRepository and JpaRepository?
CrudRepository provides basic CRUD operations. JpaRepository extends it (through PagingAndSortingRepository and ListCrudRepository) and adds JPA-specific and batch features like flush(), saveAndFlush(), deleteInBatch and List-returning finders plus paging and sorting. Most Spring Boot apps extend JpaRepository for the fuller API.
What is the N+1 select problem in Spring Data JPA?
It happens when you load N parent entities and then trigger one extra query per parent to fetch a lazy association — 1 query for the parents plus N for the children. You fix it with a fetch join in @Query, an @EntityGraph, or batch fetching, so the children load in one or few queries.
What does @Transactional readOnly=true actually do?
It hints that the transaction won't modify data, letting Hibernate skip dirty checking and flush, and letting the database/driver optimize (for example routing to a read replica). It's a performance and intent optimization for read paths, not a hard write-prevention guarantee.
Why do I get LazyInitializationException?
Because you accessed a LAZY association after the persistence context (the session) closed — typically outside the transactional service method, in the controller or view. Fix it by fetching what you need inside the transaction with a fetch join or entity graph, not by making everything EAGER.
How does pagination work in Spring Data JPA?
Pass a Pageable (PageRequest.of(page, size, sort)) to a repository method and return Page<T> or Slice<T>. Page runs an extra count query for total elements; Slice doesn't, so Slice is cheaper when you only need next/previous navigation.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — See the Java Full Stack course in Hyderabad

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 16 July 2026 LinkedIn
Chat with us