Skip to main content

Chapter 1: Interview Questions — JPA & Database

Objective

This chapter covers the most frequent questions about JPA, Hibernate, entity relationships, and database performance issues. Each question includes a simplified answer and a key phrase to say in an interview.


Q1. What is the N+1 problem in JPA?​

The restaurant analogy​

Imagine you ask a waiter at a restaurant:

"Bring me the 10 orders"

Then, for each order, you ask again:

"And who is the customer for this order?"

You don't make 1 big request. You make:

  • 1 request for the orders
  • then 10 small requests for the customers

That's a lot of unnecessary back-and-forth.

What is N+1 concretely?​

1 query for the main list
+ N queries for the related data
────────────────────────────────
= N + 1 queries in total

If N = 10 orders:

SELECT * FROM orders;                         -- 1 query
SELECT * FROM customers WHERE id = 1; -- query 2
SELECT * FROM customers WHERE id = 2; -- query 3
SELECT * FROM customers WHERE id = 3; -- query 4
...
SELECT * FROM customers WHERE id = 10; -- query 11

Total = 11 queries, whereas one would have sufficed.

Why does it happen?​

Because often the order.customer relationship is loaded in lazy loading. JPA doesn't load the customer initially — it waits for you to do order.getCustomer().

The code that causes N+1​

List<Order> orders = orderRepository.findAll();

for (Order o : orders) {
System.out.println(o.getCustomer().getName());
// ↑ EACH call triggers an SQL query!
}

Here:

  • findAll() loads the orders → 1 query
  • getCustomer() triggers 1 query per order → N queries

The solution: JOIN FETCH​

@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomers();

JPA makes a single query that brings back both the orders and their customers.

Alternative: @EntityGraph​

@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();

Alternative: @BatchSize​

@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 50)
private Customer customer;
}

Instead of making N queries, JPA makes N/50 (loads customers in batches of 50).

Simple mental image​

ApproachTrips
Without correction (N+1)11 trips for 10 orders
With JOIN FETCH1 single trip for everything
Interview phrase

EN: "The N+1 problem happens when JPA fires one query per related entity. I fix it with JOIN FETCH or @EntityGraph to load everything in a single query."

FR: "Le problème N+1 arrive quand JPA exécute une requête initiale, puis une requête supplémentaire pour chaque entité liée. Je le corrige avec JOIN FETCH ou @EntityGraph pour tout charger en une seule requête."


Q2. What is the difference between Lazy Loading and Eager Loading?​

StrategyBehaviorDefault for
LAZYLoads related data only when accessed@OneToMany, @ManyToMany
EAGERLoads related data immediately with the entity@ManyToOne, @OneToOne
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;

@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
Common pitfall

EAGER everywhere = performance disaster. If each entity loads its 5 relations eagerly, a simple query can trigger hundreds of cascading queries.

Best practice: Everything in LAZY + JOIN FETCH when necessary.

Interview phrase

"I default everything to LAZY loading and use JOIN FETCH or EntityGraph when I need to eagerly load specific associations for a use case."


Q3. What is the difference between JpaRepository, CrudRepository, and PagingAndSortingRepository?​

JpaRepository (most complete)
↑ inherits from
PagingAndSortingRepository (pagination + sorting)
↑ inherits from
CrudRepository (basic CRUD)
↑ inherits from
Repository (marker interface)
InterfaceAdded methods
CrudRepositorysave(), findById(), findAll(), deleteById(), count()
PagingAndSortingRepositoryfindAll(Pageable), findAll(Sort)
JpaRepositoryflush(), saveAndFlush(), deleteInBatch(), returns List<T> instead of Iterable<T>

In practice, we almost always use JpaRepository.


Q4. Explain the propagation strategies of @Transactional​

PropagationBehavior
REQUIRED (default)Joins the existing transaction or creates a new one
REQUIRES_NEWAlways creates a new transaction (suspends the existing one)
SUPPORTSJoins if existing, otherwise executes without transaction
NOT_SUPPORTEDAlways executes without transaction
MANDATORYExisting transaction required, otherwise exception
NEVERNo transaction, otherwise exception
NESTEDNested transaction (savepoint)

Concrete use case for REQUIRES_NEW​

@Transactional
public void processPayment(Order order) {
paymentGateway.charge(order); // Main transaction
auditService.logPayment(order); // If this fails, payment must still persist
}

@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPayment(Order order) {
// New independent transaction
// Even if this transaction fails, the payment is saved
}
}
Interview phrase

"I use REQUIRED by default. I switch to REQUIRES_NEW when I need an independent transaction, like audit logging that must persist regardless of the main transaction's outcome."


Q5. Difference between @Transactional(readOnly = true) and @Transactional​

@Transactional(readOnly = true)   // Read: Hibernate disables dirty checking
public List<User> findAll() { ... }

@Transactional // Write: commit if success, rollback if exception
public User create(UserDto dto) { ... }

Best practice: Put @Transactional(readOnly = true) at the class level, and @Transactional on write methods:

@Service
@Transactional(readOnly = true)
public class UserService {

public List<User> findAll() { ... } // Inherits readOnly = true
public User findById(Long id) { ... } // Inherits readOnly = true

@Transactional // Override: write
public User create(UserDto dto) { ... }

@Transactional // Override: write
public void delete(Long id) { ... }
}

Q6. How to implement pagination in Spring Data JPA?​

// Repository — nothing to do, JpaRepository supports Pageable natively
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByRole(Role role, Pageable pageable);
}

// Controller
@GetMapping("/users")
public Page<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return userRepository.findAll(pageable);
}

The returned Page<T> contains: content, page number, size, total elements, number of pages.


Q7. What are Query Methods in Spring Data JPA?​

Spring Data automatically generates SQL queries from the method name:

public interface UserRepository extends JpaRepository<User, Long> {

List<User> findByName(String name);
// → SELECT * FROM users WHERE name = ?

List<User> findByNameAndEmail(String name, String email);
// → SELECT * FROM users WHERE name = ? AND email = ?

List<User> findByAgeBetween(int min, int max);
// → SELECT * FROM users WHERE age BETWEEN ? AND ?

List<User> findByNameContainingIgnoreCase(String keyword);
// → SELECT * FROM users WHERE LOWER(name) LIKE LOWER('%keyword%')

Optional<User> findByEmail(String email);
// → SELECT * FROM users WHERE email = ? LIMIT 1

long countByRole(Role role);
// → SELECT COUNT(*) FROM users WHERE role = ?

boolean existsByEmail(String email);
// → SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END FROM users WHERE email = ?
}
Interview phrase

"Spring Data JPA derives queries from method names. For complex queries, I use @Query with JPQL or native SQL."


Q8. When to use @Query vs Query Methods?​

ApproachWhen to use
Query MethodsSimple queries (1-2 conditions)
@Query (JPQL)Complex queries, JOIN, aggregations
@Query (native)DBMS-specific SQL queries, critical performance
// JPQL
@Query("SELECT u FROM User u WHERE u.department.name = :dept AND u.active = true")
List<User> findActiveByDepartment(@Param("dept") String dept);

// Native SQL
@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List<User> findRecentUsers();

Q9. How to manage entity relationships?​

@ManyToOne / @OneToMany (most common)​

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}

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

@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
}

@ManyToMany​

@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
Interview trap

Always use Set for @ManyToMany, never List — otherwise Hibernate may generate inefficient queries (deletes everything then reinserts).


⬅️ Back to Module 18 | Chapter 2: Refactoring Exercises →