Overview
Most Spring apps talk to a relational DB through Spring Data JPA + Hibernate. Interviews drill on repositories, @Transactional (where it works and where it silently doesn’t), propagation, and the N+1 query problem.
Official: Spring Data JPA, @Transactional.
Table of contents
- Repositories
@Transactional: where it works- Propagation & isolation
readOnly = trueLazyInitializationException- N+1 problem
- Optimistic vs pessimistic locking
- Interview questions
- Summary
1. Repositories
Extend JpaRepository<Entity, ID> and you get CRUD for free:
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByStatusAndCreatedAtAfter(String status, Instant when);
@Query("select o from Order o where o.customer.id = :id")
List<Order> forCustomer(@Param("id") Long id);
}
Method names are translated to queries automatically; use @Query for anything beyond simple lookups.
2. @Transactional: where it works
Spring wraps your @Service bean in a proxy. When another bean calls a @Transactional method through that proxy, Spring opens a transaction, commits on success, rolls back on RuntimeException.
Common reasons it silently doesn’t work:
| Cause | Fix |
|---|---|
Self-invocation — this.save() inside the same class |
Move the method to another bean |
Method is private or package-private |
Make it public |
You used new MyService() yourself |
Let Spring create the bean |
| Bean isn’t a Spring bean at all | Add @Service (or similar) |
Spring rolls back on unchecked exceptions by default. To roll back on a checked exception: @Transactional(rollbackFor = MyChecked.class).
3. Propagation & isolation
| Propagation | Meaning |
|---|---|
| REQUIRED (default) | Join the current transaction, or start one |
| REQUIRES_NEW | Suspend the outer transaction, start a fresh one |
| NESTED | Use a savepoint within the current transaction |
| NOT_SUPPORTED | Run with no transaction |
| MANDATORY | Must already be in a transaction, else error |
Isolation: READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE. Higher isolation → more locks and retries. Default is usually whatever the DB defaults to (PostgreSQL = read committed, MySQL InnoDB = repeatable read).
4. readOnly = true
@Transactional(readOnly = true)
public List<Order> recent() { ... }
Hints Hibernate to skip dirty-checking and flush for that transaction. Cheap win on query-only methods.
5. LazyInitializationException
Happens when you access a lazy association outside an open Hibernate session — typically in the controller after the service returned.
Fixes:
- Extend the transaction (
@Transactionalon the service method that loads the graph). - Use a JOIN FETCH in your JPQL.
- Use an
@EntityGraphon the repository method. - Return a DTO/projection instead of the entity.
6. N+1 problem
Symptom: loading 100 orders triggers 1 query for orders + 100 queries for each order’s customer.
Fixes:
| Fix | When |
|---|---|
@EntityGraph(attributePaths = "customer") on the repo method |
Simple eager fetch for a specific use case |
JOIN FETCH in JPQL |
More control |
@BatchSize on the association |
Groups N queries into batches |
| DTO projection | Best when you only need a subset of fields |
Default to @ManyToOne(fetch = LAZY) and pull eagerly only where the controller actually needs it.
7. Optimistic vs pessimistic locking
- Optimistic: add a
@Versionfield. Spring/Hibernate throwsOptimisticLockExceptionif another transaction updated the row first. Cheap, no DB locks. - Pessimistic:
@Lock(LockModeType.PESSIMISTIC_WRITE)on the repository method → emitsSELECT ... FOR UPDATE. Use sparingly.
8. Interview questions
Q: Why isn’t @Transactional working?
A: Most often self-invocation (this.method()), a non-public method, or calling a non-Spring-managed instance. Fix by calling through another bean and making the method public.
Q: REQUIRED vs REQUIRES_NEW? A: REQUIRED joins the existing transaction. REQUIRES_NEW suspends the outer one and starts a fresh transaction — useful for audit logs that must commit even if the main work rolls back.
Q: What’s the N+1 problem?
A: Loading a parent collection triggers one query per child fetch. Fix with @EntityGraph, JOIN FETCH, batching, or DTO projections.
Q: What is LazyInitializationException?
A: A lazy association accessed after the Hibernate session has closed. Extend the transaction or fetch eagerly in the query.
Q: What does readOnly = true do?
A: Tells Hibernate to skip dirty checking and flush. Optimization for query-only methods.
Q: Default rollback rule?
A: Rolls back on RuntimeException and Error, not on checked exceptions unless you set rollbackFor.
9. Summary
| Topic | One line |
|---|---|
| Tx proxy | Public method, called through another bean |
| Propagation | Know REQUIRED + REQUIRES_NEW |
| readOnly | Cheap optimisation for queries |
| N+1 | Entity graph / JOIN FETCH / DTO |
| Locking | @Version first, PESSIMISTIC_WRITE only when needed |
Next steps
- Pitfalls & proxies — why self-invocation breaks
@Transactional - Database indexes — SQL side of performance
Happy Learning