Data Access (JPA) & Transactions

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

  1. Repositories
  2. @Transactional: where it works
  3. Propagation & isolation
  4. readOnly = true
  5. LazyInitializationException
  6. N+1 problem
  7. Optimistic vs pessimistic locking
  8. Interview questions
  9. 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-invocationthis.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 (@Transactional on the service method that loads the graph).
  • Use a JOIN FETCH in your JPQL.
  • Use an @EntityGraph on 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 @Version field. Spring/Hibernate throws OptimisticLockException if another transaction updated the row first. Cheap, no DB locks.
  • Pessimistic: @Lock(LockModeType.PESSIMISTIC_WRITE) on the repository method → emits SELECT ... 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


Happy Learning

Next: Spring Security — Filter Chain & JWT