Chapter 1: Interview Questions — JPA & Database
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 querygetCustomer()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
| Approach | Trips |
|---|---|
| Without correction (N+1) | 11 trips for 10 orders |
| With JOIN FETCH | 1 single trip for everything |
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?
| Strategy | Behavior | Default for |
|---|---|---|
| LAZY | Loads related data only when accessed | @OneToMany, @ManyToMany |
| EAGER | Loads related data immediately with the entity | @ManyToOne, @OneToOne |
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
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.
"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)
| Interface | Added methods |
|---|---|
| CrudRepository | save(), findById(), findAll(), deleteById(), count() |
| PagingAndSortingRepository | findAll(Pageable), findAll(Sort) |
| JpaRepository | flush(), saveAndFlush(), deleteInBatch(), returns List<T> instead of Iterable<T> |
In practice, we almost always use JpaRepository.
Q4. Explain the propagation strategies of @Transactional
| Propagation | Behavior |
|---|---|
REQUIRED (default) | Joins the existing transaction or creates a new one |
REQUIRES_NEW | Always creates a new transaction (suspends the existing one) |
SUPPORTS | Joins if existing, otherwise executes without transaction |
NOT_SUPPORTED | Always executes without transaction |
MANDATORY | Existing transaction required, otherwise exception |
NEVER | No transaction, otherwise exception |
NESTED | Nested 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
}
}
"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 = ?
}
"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?
| Approach | When to use |
|---|---|
| Query Methods | Simple 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<>();
}
Always use Set for @ManyToMany, never List — otherwise Hibernate may generate inefficient queries (deletes everything then reinserts).