Series of Backend Developer Interview Questions Part 8
This article is part of a hands-on series of backend developer interview questions, focusing on real production problems rather than textbook theory. It dives deep into JPA performance pitfalls such as N+1 queries, EntityGraph vs fetch join, casc...

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.
1. Question 1: When should you use EntityGraph instead of fetch join?
join fetch and felt like a hero… until pagination exploded or Hibernate started yelling about “multiple bag fetch”, welcome to the club. A fetch join is great when the query’s shape is stable and you’re intentionally pulling a specific association in the same SQL, right now, for this one use case. The moment you need the same repository method to sometimes load extra relationships and sometimes not, EntityGraph tends to win because it lets you keep the query semantics (filtering/sorting) separate from the fetching plan. In other words: fetch join hardcodes the loading strategy into the query, while EntityGraph can be applied declaratively (or dynamically) without rewriting the JPQL every time.
EntityGraph is also a safer default for “list screens” where you need pagination and only want to avoid lazy-loading surprises. fetch join with a collection often creates duplicated root rows and forces Hibernate into awkward workarounds; it can work, but it’s easy to accidentally get wrong results or poor performance. With EntityGraph, you can keep a clean findAll(Pageable)-style query and let the provider fetch the requested attributes according to the graph. It’s not magic—loading a big graph still costs real SQL—but the control surface is nicer: you’re describing what to load rather than rewriting the whole query.
@EntityGraph to load items and each item’s product, while still allowing pageable queries without embedding join fetch into every JPQL string:
@Entity
class Order {
@Id Long id;
@ManyToOne(fetch = FetchType.LAZY)
Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
List<orderitem> items = new ArrayList<>();
}
@Entity
class OrderItem {
@Id Long id;
@ManyToOne(fetch = FetchType.LAZY)
Order order;
@ManyToOne(fetch = FetchType.LAZY)
Product product;
int quantity;
}
public interface OrderRepository extends JpaRepository<order, long=""> {
@EntityGraph(attributePaths = {"items", "items.product"})
Page<order> findByCustomerId(Long customerId, Pageable pageable);
// Same filtering, different fetching plan:
Page<order> findByCustomerId(Long customerId, Pageable pageable);
}
join fetch, because you’d typically have to duplicate JPQL queries or build dynamic JPQL strings. Also, when you later realize you only need customer but not items, EntityGraph lets you change that without touching the query logic that decides which orders to return.
https://www.baeldung.com/jpa-entity-graph
2. Question 2: What are the pros and cons of using CascadeType.ALL?
CascadeType.ALL is like giving your entity relationship a master key and then acting surprised when it opens doors you didn’t mean to unlock. The main benefit is convenience: persistence operations on the parent automatically propagate to children—persist, merge, remove, refresh, detach. That can dramatically reduce boilerplate in true “aggregate” relationships where the child’s lifecycle is fully owned by the parent. If an Order owns OrderItems and items should never exist without an order, cascading persist and remove can be exactly what you want.
orderRepository.delete(order) and suddenly items disappear too, which may be correct… unless those “items” were actually shared references, or you later changed the domain rule and forgot the mapping. Another subtle cost is merge: cascading merge across a deep graph can trigger a lot of selects/updates and make a simple update endpoint feel like it’s dragging a piano up the stairs.
PERSIST and MERGE but you intentionally avoid REMOVE so deletes must be explicit (and reviewable). You also typically pair ownership semantics with orphanRemoval = true rather than relying on REMOVE for every case, because orphan removal expresses “child removed from the collection should be deleted”, which is usually closer to business intent.
@Entity
class Order {
@Id @GeneratedValue Long id;
@OneToMany(mappedBy = "order",
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
orphanRemoval = true)
private List<orderitem> items = new ArrayList<>();
public void addItem(Product p, int qty) {
OrderItem item = new OrderItem(this, p, qty);
items.add(item);
}
public void removeItem(Long itemId) {
items.removeIf(i -> i.getId().equals(itemId)); // orphanRemoval deletes it
}
}
@Entity
class OrderItem {
@Id @GeneratedValue Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
private Product product;
private int quantity;
protected OrderItem() {}
public OrderItem(Order order, Product product, int quantity) {
this.order = order;
this.product = product;
this.quantity = quantity;
}
}
persist/merge cascade), but deletes are a deliberate operation on the aggregate rather than a side effect of delete(order) accidentally nuking related data. That’s the real interview-level point: cascading is not a JPA trick, it’s a domain commitment—if you wouldn’t say it confidently in plain English, you probably shouldn’t encode it as ALL.
3. Question 3: Why is batch insert/update in JPA hard?
flush() to send SQL, clear() to drop managed instances, and keep the transaction boundaries reasonable.
@Service
public class BulkImportService {
private final EntityManager em;
public BulkImportService(EntityManager em) {
this.em = em;
}
@Transactional
public void insertCustomers(List<customer> customers) {
int batchSize = 50;
for (int i = 0; i < customers.size(); i++) {
em.persist(customers.get(i));
if (i > 0 && i % batchSize == 0) {
em.flush(); // executes batched INSERTs
em.clear(); // prevents persistence-context memory blow-up
}
}
em.flush();
em.clear();
}
}
default_batch_fetch_size / @BatchSize):
https://prasanthmathialagan.wordpress.com/2017/04/20/beware-of-hibernate-batch-fetching/
4. Question 4: When should you choose WebFlux instead of Spring MVC?
@RestController
@RequestMapping("/api/profile")
public class ProfileController {
private final WebClient webClient;
public ProfileController(WebClient.Builder builder) {
this.webClient = builder.baseUrl("https://downstream.internal").build();
}
@GetMapping("/{userId}")
public Mono<profileview> profile(@PathVariable String userId) {
Mono<userdto> userMono = webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(UserDto.class);
Mono<list<orderdto>> ordersMono = webClient.get()
.uri("/orders?userId={id}", userId)
.retrieve()
.bodyToFlux(OrderDto.class)
.collectList();
return Mono.zip(userMono, ordersMono)
.map(tuple -> new ProfileView(tuple.getT1(), tuple.getT2()));
}
public record ProfileView(UserDto user, List<orderdto> orders) {}
}
https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html
5. Question 5: How does Spring Security’s filter chain work?
FilterChainProxy, which is itself typically reached via DelegatingFilterProxy in the servlet container. This design matters because it allows Spring Security to choose which security filter chain applies to a given request and to keep everything consistent with the Spring ApplicationContext lifecycle. (Home)
springSecurityFilterChain, then FilterChainProxy selects a matching SecurityFilterChain (based on request matchers), and runs filters in order. Filters do very specific jobs: extracting credentials, establishing an Authentication, storing it in the SecurityContext, enforcing authorization, handling exceptions, and so on. If you ever need to debug “why is my request 403?”, putting a breakpoint or enabling debug logs around the proxy/chain selection is usually more productive than guessing in the controller, because your controller is often innocent—it’s just being blocked at the door. (Home)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.addFilterAfter(new PrincipalLoggingFilter(), BasicAuthenticationFilter.class)
.build();
}
static class PrincipalLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
System.out.println("Authenticated as: " + auth.getName());
}
filterChain.doFilter(request, response);
}
}
}
BasicAuthenticationFilter” means your log sees an established Authentication (if credentials were valid). If you put it too early, you’ll log null and wonder if Spring Security is broken—spoiler: it’s not broken, it’s just earlier in the chain.
https://docs.spring.io/spring-security/reference/servlet/architecture.html
Read more at : Series of Backend Developer Interview Questions Part 8





