What Is Spring Data JPA
Spring Data JPA is an abstraction layer that sits on top of the Java Persistence API (JPA) and removes most of the boilerplate involved in writing data access code. Instead of implementing DAOs by hand, you declare a repository interface and Spring Data generates the implementation at runtime.
To keep three terms straight:
| Term | What It Is |
|---|---|
| JPA | A Java specification (interfaces and annotations) for ORM. Defines EntityManager, @Entity, JPQL. |
| Hibernate | The most common JPA provider — the actual implementation that talks to the database. |
| Spring Data JPA | A Spring project that auto-implements repository interfaces on top of any JPA provider (usually Hibernate). |
Spring Data JPA does not replace Hibernate — it sits above it. When you call userRepository.findById(1L), Spring Data delegates to the JPA EntityManager, which delegates to Hibernate, which executes SQL.
Each layer delegates downward and depends only on the layer immediately below — you can swap Hibernate for EclipseLink without touching Spring Data, and you can drop Spring Data entirely and call JPA directly when needed.
On the 2V0-72.22 exam, Spring Data JPA falls under the Data Management section, which carries roughly 10-12% of all questions. Expect questions on the repository hierarchy, @Transactional propagation, rollback rules, derived query method naming, and the difference between JdbcTemplate and JPA-based access.
This guide walks through everything in that section, with code examples and the exam traps that cost candidates points.
Setup & Auto-configuration
To use Spring Data JPA in a Spring Boot application, add the starter:
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Gradle:
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'Once the starter is on the classpath, Spring Boot auto-configures everything you need: a DataSource, an EntityManagerFactory, a PlatformTransactionManager, and @EnableJpaRepositories (so it scans for repository interfaces).
In a plain Spring application (no Boot), you have to enable repository scanning yourself:
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repo")
public class JpaConfig { /* ... */ }If you want to dig into how JpaRepositoriesAutoConfiguration and DataSourceAutoConfiguration decide what to register, see Spring Boot Auto-configuration Explained.
Exam tip:
@EnableJpaRepositoriesis applied automatically by Spring Boot. You only add it manually in non-Boot Spring applications, or to override the default scan path.
EntityManager and @PersistenceContext
You rarely touch EntityManager directly when using Spring Data, but the exam expects you to know how it gets injected when you do — for example in a custom repository implementation.
@Repository
public class OrderQueryDao {
@PersistenceContext
private EntityManager em;
public List<Order> findRecent(int limit) {
return em.createQuery("SELECT o FROM Order o ORDER BY o.createdAt DESC", Order.class)
.setMaxResults(limit)
.getResultList();
}
}@PersistenceContext does not inject the raw EntityManager — it injects a thread-safe shared proxy that delegates to the real EntityManager bound to the current transaction. That's why a singleton DAO can safely hold an EntityManager field.
Exam tip: Use
@PersistenceContext, not@Autowired, to injectEntityManager. The proxy created by@PersistenceContextis what makesEntityManagersafe to use as a field in a singleton bean.
Exam tip:
EntityManageris not thread-safe by itself. The Spring/JPA injection magic gives you a transaction-scoped proxy, not the underlying instance.
Repository Hierarchy
Every Spring Data repository extends from a small family of marker interfaces. The exam expects you to know what each level adds.
| Interface | Extends | What It Adds |
|---|---|---|
Repository<T, ID> | — | Marker interface. Spring detects all sub-interfaces for repository proxy creation. No methods. |
CrudRepository<T, ID> | Repository | Basic CRUD: save, findById, findAll, count, delete, existsById. |
ListCrudRepository<T, ID> | CrudRepository | Same as CrudRepository, but returns List instead of Iterable. |
PagingAndSortingRepository<T, ID> | Repository | Adds findAll(Pageable) and findAll(Sort). |
JpaRepository<T, ID> | ListCrudRepository, ListPagingAndSortingRepository, QueryByExampleExecutor | Adds JPA-specific operations: flush, saveAndFlush, deleteAllInBatch, getReferenceById. |
Version note: This hierarchy reflects Spring Data 3.x (released late 2022, used with Spring Framework 6 / Spring Boot 3). In Spring Data 2.x,
PagingAndSortingRepositoryextendedCrudRepositoryandJpaRepositoryextendedPagingAndSortingRepositorydirectly. If your study materials reference the older layout, that is why.
Visualised (Spring Data 3.x):
Spring Data creates a proxy implementation of your interface at startup. The proxy is itself a Spring bean — created during the bean lifecycle like any other component, with all the BeanPostProcessor hooks applied.
A typical repository declaration:
public interface UserRepository extends JpaRepository<User, Long> {
// No code needed — Spring Data implements all CRUD and JPA methods automatically
}You can also extend just CrudRepository if you want a leaner API and don't need JPA-specific methods:
public interface ProductRepository extends CrudRepository<Product, Long> {
Optional<Product> findBySku(String sku);
}Exam tip:
JpaRepositoryextendsListCrudRepositoryandListPagingAndSortingRepository. It is the JPA-specific terminal interface — it addsflush(),saveAndFlush(),deleteAllInBatch(), andgetReferenceById()(which returns a lazy proxy without hitting the database).
Exam tip: The
@Repositoryannotation is not required on Spring Data interfaces. Spring detects them via@EnableJpaRepositories(or Boot's auto-config) and creates the proxy automatically.@Repositoryis for traditional@Component-style DAOs.
Paging and Sorting
PagingAndSortingRepository (and therefore JpaRepository) gives you Pageable and Sort for free:
public interface OrderRepository extends JpaRepository<Order, Long> {
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
List<Order> findByCustomerId(Long customerId, Sort sort);
}
// Usage
Pageable page = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Order> results = orderRepository.findByStatus(OrderStatus.OPEN, page);
results.getTotalElements(); // total row count (executes COUNT query)
results.getTotalPages(); // total page count
results.getContent(); // List<Order> for current pageSpring Data offers three return types for paginated queries:
| Return Type | Executes COUNT Query | Use When |
|---|---|---|
Page<T> | Yes | You need total count / total pages (UI pagination with "page X of Y") |
Slice<T> | No | "Load more" UX — you only need to know if a next page exists |
List<T> | No | You just want the slice without metadata |
Exam tip:
Page<T>triggers an extraSELECT COUNT(*)query to compute total count. If you don't need the total, returnSlice<T>to avoid the cost.
Derived Query Methods
The most distinctive feature of Spring Data is query derivation from method names. You declare a method following a naming convention and Spring Data generates the JPQL query at startup.
The pattern is:
<verb> [Distinct] [Top<N> | First<N>] By <Property> [Operator] [And|Or <Property> [Operator]] [OrderBy <Property> Asc|Desc]
Verbs (subject keywords):
| Verb | Purpose | Return Type |
|---|---|---|
find...By / read...By / get...By / query...By | Read | Entity, Optional<Entity>, List<Entity>, Page<Entity>, Stream<Entity> |
count...By | Count rows | long |
exists...By | Check existence | boolean |
delete...By / remove...By | Delete | void or long (count of deleted rows) |
Operator keywords (predicate):
| Keyword | Translates To |
|---|---|
Is, Equals | = ? (default if omitted) |
Not | <> ? |
LessThan, LessThanEqual | < ?, <= ? |
GreaterThan, GreaterThanEqual | > ?, >= ? |
Between | BETWEEN ? AND ? |
Like, NotLike | LIKE ? |
StartingWith, EndingWith, Containing | LIKE 'x%', LIKE '%x', LIKE '%x%' |
In, NotIn | IN (?), NOT IN (?) |
IsNull, IsNotNull | IS NULL, IS NOT NULL |
True, False | = true, = false |
IgnoreCase | LOWER(x) = LOWER(?) |
OrderBy<Property>Asc / Desc | ORDER BY x ASC / DESC |
Examples building from simple to complex:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByLastNameAndFirstName(String last, String first);
List<User> findByAgeBetween(int min, int max);
List<User> findByEmailContainingIgnoreCase(String fragment);
List<User> findByStatusInOrderByCreatedAtDesc(Collection<Status> statuses);
long countByStatus(Status status);
boolean existsByEmail(String email);
// Property path traversal — User has an Address, Address has a city
List<User> findByAddress_City(String city);
// Limiting results
Optional<User> findFirstByOrderByCreatedAtDesc();
List<User> findTop5ByStatusOrderByCreatedAtDesc(Status status);
}Exam tip: Derived query method names are validated at application startup. A typo in a property name (
findByEmial) will throwPropertyReferenceExceptionand the context will fail to start. Catch these in tests, not in production.
Exam tip: When a property name is ambiguous (e.g.,
addressCitycould meanaddress.cityor a property literally namedaddressCity), use an underscore as an explicit separator:findByAddress_City.
@Query — JPQL and Native
Derived methods cover most cases, but they get unwieldy with more than two or three predicates. For anything more complex, use @Query.
JPQL with named parameters:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o WHERE o.customer.email = :email AND o.status = :status")
List<Order> findByCustomerEmailAndStatus(@Param("email") String email,
@Param("status") OrderStatus status);
}JPQL with positional parameters:
@Query("SELECT o FROM Order o WHERE o.total > ?1")
List<Order> findOrdersOverAmount(BigDecimal amount);Native SQL:
@Query(value = "SELECT * FROM orders WHERE total > :amount",
nativeQuery = true)
List<Order> findOrdersOverAmountNative(@Param("amount") BigDecimal amount);Modifying queries (UPDATE / DELETE):
@Modifying
@Transactional
@Query("UPDATE User u SET u.status = :status WHERE u.lastLoginAt < :cutoff")
int deactivateInactiveUsers(@Param("status") Status status,
@Param("cutoff") Instant cutoff);Exam tip:
@Modifyingqueries must run inside a transaction. If the calling code has no@Transactional, you will getTransactionRequiredException. Either annotate the method with@Transactionalor rely on a transactional service layer.
Exam tip: Native queries (
nativeQuery = true) bypass JPQL parsing. They are not portable across databases —LIMITworks in PostgreSQL/MySQL but not Oracle (which usesROWNUMorFETCH FIRST).
@Transactional Deep Dive
@Transactional is the most heavily tested topic in the Data Management section. Expect 2-4 questions on propagation, rollback rules, or self-invocation.
How It Works
@Transactional is implemented through Spring AOP. When you put @Transactional on a method, Spring wraps the bean in a proxy. The proxy intercepts the call, asks a PlatformTransactionManager to start (or join) a transaction, invokes the target method, and commits or rolls back based on the outcome.
If you are unsure how proxying works, read the Spring AOP and Pointcut Expressions guide first — every @Transactional gotcha on the exam is really an AOP proxy gotcha.
Where to Put It
@Transactional can go on a class (applies to all public methods) or on individual methods (overrides class-level setting).
@Service
@Transactional(readOnly = true) // class-level default
public class OrderService {
public Order findById(Long id) { /* read-only TX */ }
@Transactional // overrides — full read-write TX
public Order placeOrder(NewOrderRequest req) { /* ... */ }
}Two @Transactional Annotations — Watch the Import
There are two annotations called @Transactional and the exam tests whether you can tell them apart:
| Annotation | Package | Origin | Rollback Attribute |
|---|---|---|---|
| Spring's | org.springframework.transaction.annotation.Transactional | Spring Framework | rollbackFor / noRollbackFor |
| Jakarta's | jakarta.transaction.Transactional (formerly javax.transaction.Transactional) | Jakarta EE / JTA | rollbackOn / dontRollbackOn |
Spring respects both, but only the Spring one supports propagation, isolation, timeout, and readOnly fully. The Jakarta one has a smaller attribute set and uses different keywords. Mixing them — or assuming the Jakarta one accepts rollbackFor — is a common exam trap.
Exam tip: When code uses
@Transactionalwithout explicit attributes, the import line decides the semantics. Always check whether it isorg.springframework...orjakarta.transaction...before answering propagation/rollback questions.
Propagation
Propagation determines what happens when a transactional method is called from another transactional context. Spring supports seven values:
| Propagation | If a TX exists | If no TX exists | Typical Use |
|---|---|---|---|
REQUIRED (default) | Join the existing TX | Start a new TX | Default for service methods |
REQUIRES_NEW | Suspend the outer TX, start a new independent TX | Start a new TX | Audit logging that must commit even if caller rolls back |
SUPPORTS | Join the existing TX | Run without a TX | Helper methods that work either way |
NOT_SUPPORTED | Suspend the existing TX, run without TX | Run without TX | Long-running non-DB work inside a transactional caller |
MANDATORY | Join the existing TX | Throw IllegalTransactionStateException | Enforce that the caller already started a TX |
NEVER | Throw IllegalTransactionStateException | Run without TX | Enforce no surrounding TX |
NESTED | Create a savepoint inside the existing TX | Start a new TX | Partial rollback within a parent transaction |
The three most-tested values behave very differently when an outer transactional method calls an inner transactional method. Visualised:
- ►1 transaction. Rollback anywhere = full rollback.
- ►Inner cannot commit independently of outer.
- ►2 independent transactions, separate connections.
- ►Inner commits even if outer rolls back (audit logs).
- ►1 transaction. Inner can roll back to the savepoint without aborting outer.
- ⚠Requires JDBC savepoint support (DataSourceTransactionManager). NOT supported by JpaTransactionManager out of the box.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog(AuditEvent event) {
// Always commits independently of the caller's transaction
}Exam tip:
REQUIREDis the default. If a question doesn't mention propagation explicitly, assumeREQUIRED.
Exam tip:
REQUIRES_NEWsuspends the outer transaction — it doesn't nest it. Both transactions hold their own connections. If both try to write the same row, you get a self-deadlock.
Exam tip:
NESTEDrequires a JDBC driver andDataSourcethat support savepoints. It is not supported by JPA'sJpaTransactionManagerout of the box.
Isolation
Isolation controls how concurrent transactions see each other's uncommitted changes.
| Isolation | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
READ_UNCOMMITTED | Possible | Possible | Possible |
READ_COMMITTED | Prevented | Possible | Possible |
REPEATABLE_READ | Prevented | Prevented | Possible |
SERIALIZABLE | Prevented | Prevented | Prevented |
DEFAULT | Uses the database default (PostgreSQL: READ_COMMITTED, MySQL: REPEATABLE_READ) |
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Report buildReport() { /* ... */ }Exam tip: Higher isolation = more locking = lower concurrency. The default (
DEFAULT) uses the database's own setting; do not assume it isREAD_COMMITTEDeverywhere.
Rollback Rules
By default, Spring rolls back on RuntimeException and Error. It does NOT roll back on checked exceptions.
This is the single most-missed rule on the exam.
@Transactional
public void chargeCustomer(Order o) throws PaymentException {
repository.save(o);
paymentGateway.charge(o); // throws PaymentException (checked)
// The save above is COMMITTED — checked exceptions do not roll back by default
}To override:
@Transactional(rollbackFor = PaymentException.class)
public void chargeCustomer(Order o) throws PaymentException { /* ... */ }
// Or the inverse — don't roll back even on a runtime exception
@Transactional(noRollbackFor = ValidationException.class)
public void validate(Form f) { /* ... */ }Exam tip: Default rollback =
RuntimeException+Erroronly. Checked exceptions commit the transaction unless you setrollbackFor.
The Self-Invocation Pitfall
This is the classic exam trap. @Transactional works through a proxy, so a call from inside the same bean to another method on the same bean bypasses the proxy — and therefore bypasses the transaction.
@Service
public class UserService {
public void registerAndNotify(User u) {
save(u); // self-invocation — no proxy, no TX
sendWelcome(u);
}
@Transactional
public void save(User u) {
repository.save(u);
}
private void sendWelcome(User u) { /* ... */ }
}When registerAndNotify() calls this.save(), it calls the method directly on the target object. The @Transactional advice never fires. save() runs without a transaction.
Three ways to fix it:
- Move the
@Transactionalmethod to a different bean — then the call goes through the proxy. - Inject the bean into itself and call through the injected reference (works but smells).
- Use
AopContext.currentProxy()with@EnableAspectJAutoProxy(exposeProxy = true)— last resort.
// Fix #1 — split into two beans
@Service
public class UserRegistrationService {
private final UserService userService;
public void registerAndNotify(User u) {
userService.save(u); // goes through proxy → @Transactional applied
sendWelcome(u);
}
}Exam tip:
@Transactionalon aprivatemethod is silently ignored — proxies cannot intercept private methods.
Exam tip:
@Transactionalon afinalmethod is also ignored when CGLIB proxies are used — CGLIB proxies subclass the target and cannot overridefinalmethods.
Exam tip:
@Transactionalonprotectedor package-private methods is also ignored by default. Spring'sAnnotationTransactionAttributeSourceis configured withpublicMethodsOnly = trueout of the box — non-public methods are skipped regardless of proxy type.
Exam tip: If you catch the exception inside the
@Transactionalmethod and don't rethrow it, no rollback happens. The proxy only sees what bubbles out of the method.
Read-Only Optimization
@Transactional(readOnly = true)
public List<Order> findRecentOrders() { /* ... */ }readOnly = true is a hint, not enforcement:
- Hibernate skips dirty checking (no automatic flush at commit) — measurable speedup for read-heavy methods.
- Some JDBC drivers route the connection to a read replica.
- The database itself is not told to reject writes.
Exam tip:
readOnly = truedoes not prevent writes. It is a performance hint. A misbehaving method can still executeINSERTs — they just may behave unpredictably.
JdbcTemplate (Briefly)
The Data Management section also covers Spring's lower-level JDBC abstraction, JdbcTemplate. It is not deprecated and remains the right tool when you don't want JPA's overhead — for simple read paths, complex SQL, or performance-critical code.
@Repository
public class CustomerJdbcDao {
private final JdbcTemplate jdbc;
public CustomerJdbcDao(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public Customer findById(Long id) {
return jdbc.queryForObject(
"SELECT id, name, email FROM customers WHERE id = ?",
(rs, rowNum) -> new Customer(rs.getLong("id"),
rs.getString("name"),
rs.getString("email")),
id
);
}
public int updateEmail(Long id, String email) {
return jdbc.update(
"UPDATE customers SET email = ? WHERE id = ?",
email, id
);
}
}JdbcTemplate is auto-configured by Spring Boot whenever a DataSource bean exists.
Exam tip:
JdbcTemplateis thread-safe once configured and is meant to be used as a singleton bean (which is what Spring Boot gives you).
Exam tip:
JdbcTemplatetranslates JDBC's checkedSQLExceptioninto Spring's uncheckedDataAccessExceptionhierarchy. Your code never has to declarethrows SQLException.
The same translation applies to JPA exceptions — Hibernate's ConstraintViolationException, OptimisticLockException, etc. all get re-thrown as DataAccessException subclasses. The most common ones to recognize on the exam:
| Exception | Thrown When |
|---|---|
DataIntegrityViolationException | A constraint is violated (NOT NULL, FOREIGN KEY, CHECK) |
DuplicateKeyException | A unique constraint (typically primary key) is violated |
EmptyResultDataAccessException | queryForObject finds zero rows when one was expected |
IncorrectResultSizeDataAccessException | A query returns more rows than expected (e.g., 2+ for queryForObject) |
OptimisticLockingFailureException | A @Version-based optimistic lock check fails |
CannotAcquireLockException | The database refused a lock (deadlock or lock timeout) |
QueryTimeoutException | A statement exceeded its configured timeout |
All extend DataAccessException, which extends RuntimeException — so they trigger rollback by default.
Exam tip: All
DataAccessExceptionsubclasses are unchecked (RuntimeException) and trigger@Transactionalrollback by default. You don't needrollbackForto handle data access failures.
Exam tip:
JdbcTemplateintegrates with@TransactionalthroughDataSourceTransactionManager. JPA usesJpaTransactionManager. Spring Boot picks the right one based on what's on the classpath.
Common Exam Traps
A condensed checklist of the gotchas that cost points. Skim this before the exam.
- Self-invocation skips
@Transactional. Athis.foo()call on a transactional method in the same bean bypasses the proxy. @Modifyingwithout@TransactionalthrowsTransactionRequiredException.- Catching the exception inside the
@Transactionalmethod prevents rollback. The proxy only rolls back if the exception escapes. - Checked exceptions do not trigger rollback unless you set
rollbackFor. OnlyRuntimeExceptionandErrordo, by default. - Lazy loading outside a transaction throws
LazyInitializationException. Initialize collections inside the transactional method or use a fetch join. - Derived query property typos crash the application context at startup with
PropertyReferenceException. REQUIRES_NEWuses a separate connection. Two transactions touching the same row from the same thread can self-deadlock.@Transactionalonprivateorfinalmethods is silently ignored by the proxy.@Transactionalonprotected/ package-private methods is also silently ignored by default. Spring'sAnnotationTransactionAttributeSourcehaspublicMethodsOnly = trueout of the box, so non-public methods are skipped regardless of whether you use JDK dynamic or CGLIB proxies.readOnly = trueis a hint, not enforcement. A method can still write — the database will not stop it.
Frequently Asked Questions
Is Spring Data JPA on the 2V0-72.22 exam?
Yes. The Data Management section covers Spring Data JPA repositories, @Transactional (propagation, isolation, rollback rules), and JdbcTemplate. It accounts for roughly 10-12% of the exam. Plan to spend solid time on @Transactional — it is the densest topic.
What's the difference between JPA, Hibernate, and Spring Data JPA?
JPA is the specification (interfaces and annotations). Hibernate is the most common JPA provider — it implements the spec. Spring Data JPA is a Spring project that auto-implements your repository interfaces on top of any JPA provider, eliminating most DAO boilerplate.
Which propagation level is the default?
Propagation.REQUIRED. If a transaction is already in progress, the method joins it. If not, a new transaction starts.
When does @Transactional roll back?
By default, only on RuntimeException and Error. Checked exceptions commit the transaction unless you explicitly set rollbackFor = SomeCheckedException.class.
Do I need to know JdbcTemplate for the exam?
Yes — at a basic level. Know that it is thread-safe, that it translates SQLException into Spring's DataAccessException hierarchy, that it is auto-configured when a DataSource exists, and the difference between query, queryForObject, and update. Deep SQL knowledge is not required.
Next Steps
This guide covers the Data Management section of the 2V0-72.22 exam end to end. To round out the rest of the syllabus:
- Spring AOP and Pointcut Expressions — the proxy mechanism behind every
@Transactionalannotation. - Spring Bean Lifecycle Explained — how repositories and other proxies are built.
- Spring Boot Auto-configuration Explained — how
JpaRepositoriesAutoConfigurationandDataSourceAutoConfigurationwire your data layer for free. - Spring Professional Exam Questions 2025 — sample questions across the full syllabus.
When you are ready to practice with realistic exam-style questions on Spring Data JPA and @Transactional, our question bank has hundreds of items modeled on the 2V0-72.22 format — including the propagation and rollback edge cases that catch most candidates.