Key Takeaways
- Spring Data JPA reduces repetitive data-access code by creating repository implementations; it does not replace Jakarta Persistence or a provider such as Hibernate.
- Repositories support CRUD operations, queries derived from supported method names, explicit queries, pagination, and sorting.
- Entity mappings, database configuration, transaction boundaries, and fetching behavior still matter.
- Put business transactions in service methods and inspect the SQL your repositories execute.
Spring Data JPA is a Spring Data module that provides repository abstractions for Jakarta Persistence, commonly called JPA. You define repository interfaces, and Spring Data supplies implementations that delegate persistence work through JPA to a configured provider such as Hibernate. Its purpose is to remove repetitive data-access code—not to remove the need to understand persistence.
What Is Spring Data JPA?
A repository is an interface through which application code loads and stores domain objects. Spring Data JPA implements that abstraction for JPA-backed applications, supplying common operations and infrastructure for your query methods.
JPA historically meant Java Persistence API. The specification is now Jakarta Persistence: it defines entity mappings, persistence operations, and query semantics. An entity is a Java object whose state is mapped to relational data.
Application / service
|
Spring Data JPA repository
|
Jakarta Persistence APIs
|
Hibernate or another JPA provider
|
Relational databaseWhat Is a JPA Provider?
A JPA provider implements the persistence specification. Hibernate, for example, performs object-relational mapping (ORM): it connects entity state to database rows and executes persistence operations.
The distinction matters when you tune behavior. @Entity is a standard Jakarta Persistence annotation; Hibernate's @BatchSize is a provider-specific annotation for fetching multiple entities or collections in batches. Using repositories doesn't make those provider-specific decisions disappear.
Spring Data JPA vs. Hibernate
These technologies are complementary, not competing replacements.
| Technology | Responsibility | What you typically write |
|---|---|---|
| Spring Data JPA | Repository implementations and query-method infrastructure | Repository interfaces and query declarations |
| Jakarta Persistence | The standard persistence contract | Entity mappings and, when needed, EntityManager operations |
| Hibernate | A provider implementing ORM and persistence behavior | Provider-specific configuration when standard features aren't enough |
You can start with repository interfaces without learning every Hibernate API. However, entity lifecycle, transactions, fetching, and generated SQL are essential debugging skills. See the detailed Spring Data JPA vs Hibernate comparison for the separation of responsibilities.
Compatibility and the Jakarta Namespace
The example below pins a documented combination: Spring Boot 4.1.1 manages Spring Data JPA 4.1.1 through its dependency management. Boot 4.1.1 requires at least Java 17 and Spring Framework 7.0.9; this example targets Java 25. Check the Boot system requirements when choosing your application's release.
Jakarta Persistence 3.0 moved the persistence APIs from javax.persistence to jakarta.persistence, as documented in the specification's namespace change. Older examples using javax.persistence.Entity cannot simply be copied into this application. Upgrade the framework, provider, API dependencies, and imports together rather than mixing the two namespaces.
Spring Data JPA Example
Set Up the Project and Database
Create an empty Maven project with this pom.xml. Use JDK 25 and Maven 3.6.3 or later. Let Boot manage the dependency versions rather than selecting Spring Data and Hibernate versions independently.
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>jpa-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>The JPA starter includes Hibernate, Spring Data JPA, and Spring ORM. With H2 on the classpath and no datasource URL configured, Boot can configure an embedded database. Its SQL database documentation also explains configuring a production datasource.
Create src/main/resources/application.yaml:
spring:
jpa:
hibernate:
ddl-auto: create-dropThe four Java files below belong in src/main/java/com/example/jpademo/.
Define an Entity and Identifier
Create User.java:
package com.example.jpademo;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Objects;
@Entity
@Table(name = "app_users")
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true)
private String username;
protected User() {}
public User(String username) {
rename(username);
}
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public void rename(String username) {
this.username = Objects.requireNonNull(username, "username");
}
}@Id identifies the primary key, and @GeneratedValue requests a generated identifier. The protected no-argument constructor satisfies the entity construction requirement. The username mapping requests a non-null, unique column when the schema is generated; preserve those constraints in your production migrations.
Create a Repository
Create UserRepository.java:
package com.example.jpademo;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
@Query("select u from User u where u.username = :username")
Optional<User> lookupByUsername(@Param("username") String username);
Page<User> findByUsernameStartingWith(String prefix, Pageable pageable);
}User is the managed entity type; Long is its identifier type. Boot discovers repositories and entities in its auto-configuration packages, so placing the application class in com.example.jpademo keeps this example within the default scanning arrangement. For other layouts, configure repository scanning with @EnableJpaRepositories and entity scanning with @EntityScan.
CrudRepository vs. JpaRepository
| Interface | Capabilities | Choose it when |
|---|---|---|
| CrudRepository | Generic CRUD methods; findAll() returns Iterable | You want a smaller, store-neutral repository contract |
| JpaRepository | CRUD, list-returning collection methods, paging/sorting, and JPA-specific operations such as flush() | Your application needs the broader JPA repository API |
JpaRepository's API shows its inherited interfaces and operations. If list return types are your only requirement beyond basic CRUD, ListCrudRepository is another, narrower option.
Derived Queries and Explicit Queries
A derived query can be as concise as this interface fragment:
// Alternative to the Optional-returning signature above, not an overload.
User findByUsername(String username);Spring Data parses findByUsername as a lookup using the entity's username property. The plain User return type can represent a missing result with null; the runnable example uses Optional<User> to make absence explicit. Both forms expect at most one result, and multiple matches cause an IncorrectResultSizeDataAccessException. See the query return-type contracts.
Query derivation follows a grammar, not arbitrary English:
- Predicates use Java entity property names, not SQL column names.
- Supported conventions include combinations such as
And,Or,Between, andIgnoreCase. - Nested property paths are supported. For example,
findByAddress_ZipCodeexplicitly traversesaddress.zipCode, assuming those properties exist.
Check the derivation and property-path rules instead of guessing whether a method name is valid. Verify the generated query against representative data.
The repository's lookupByUsername method expresses the same equality predicate using @Query. Its query is JPQL—the Jakarta Persistence query language—so User names the entity, not the app_users table. @Param("username") binds the argument to :username.
Prefer an explicit query declaration when joins or business predicates make a derived name difficult to read. Bind values as parameters rather than constructing query text from input.
Transactions Belong Around Business Operations
Inherited repository CRUD operations have transaction configuration: read operations use readOnly = true, while other CRUD operations use ordinary write transactions. Declared query methods do not receive transaction configuration by default. The transaction documentation recommends boundaries around the unit of work.
Create UserService.java to keep loading and modifying a user in one transaction:
package com.example.jpademo;
import java.util.NoSuchElementException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository users;
public UserService(UserRepository users) {
this.users = users;
}
@Transactional
public void rename(Long id, String username) {
User user = users.findById(id)
.orElseThrow(() -> new NoSuchElementException("User " + id));
user.rename(username);
}
}A persistence context tracks managed entity instances. Here, the loaded user remains managed during rename; its changed state is synchronized to the database when the transaction commits. An additional save call isn't required for this managed update.
That doesn't mean changes to every Java entity object are automatically saved. An entity outside its persistence context is detached. Nor do several individually transactional repository calls automatically form one business transaction.
Spring Data JPA CRUD Example
Create DemoApplication.java. This runner demonstrates create, read, update, and delete, plus both query styles and pagination. Its repository calls are independent operations; the service method defines the rename transaction.
package com.example.jpademo;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
CommandLineRunner demo(UserRepository users, UserService service) {
return args -> {
User saved = users.save(new User("ada"));
service.rename(saved.getId(), "ada.lovelace");
User found = users.findById(saved.getId()).orElseThrow();
System.out.println(found.getUsername());
System.out.println(users.findByUsername("ada.lovelace").isPresent());
System.out.println(users.lookupByUsername("ada.lovelace").isPresent());
Page<User> page = users.findByUsernameStartingWith(
"ada", PageRequest.of(0, 20, Sort.by("username", "id")));
System.out.println(page.getTotalElements());
users.deleteById(saved.getId());
};
}
}Run it from the directory containing pom.xml:
mvn spring-boot:runFor an expanded application walkthrough, continue with the Spring Data JPA CRUD example.
Pagination and Sorting
PageRequest uses zero-based page numbers, so the call above requests the first page with at most 20 users. Sort.by("username", "id") orders by those entity properties in ascending order.
A Page includes totals and can require an additional count query. Return a Slice when you only need to know whether another chunk exists. For sorting without pagination, JpaRepository also provides findAll(Sort). Validate page sizes and allow-list sortable properties in HTTP-facing code.
Fetching and Performance: What Repositories Don't Fix
A short repository method can still execute expensive SQL. The Hibernate guide explains the fetching behavior behind these common problems:
- N+1 queries: one query loads a list, then accessing related entities triggers additional queries for individual results. Plan fetching with query-specific fetch joins or entity graphs rather than switching every relationship to eager loading.
- Lazy loading after the context closes: accessing an unfetched Hibernate association can throw
LazyInitializationException. Fetch the data required by the use case before leaving its transaction. - Loading too much: unbounded result lists and large managed object graphs increase memory use. Select only what the use case needs and process large datasets in bounded chunks.
For web applications, consider spring.jpa.open-in-view=false and assemble response data in the service layer. Boot otherwise enables Open EntityManager in View by default for web applications, allowing lazy loading during view rendering. Inspect SQL and query counts in integration tests, especially when adding relationships or changing fetch plans.
When Another Data-Access Approach Fits
Choose based on the persistence model you want, not the shortest repository interface. MyBatis suits SQL-first mapping with explicit queries. Spring Data JDBC offers repositories without JPA's session, lazy loading, or automatic dirty tracking. Spring Data JPA fits when you want an ORM-managed entity model and are prepared to understand its lifecycle.
Common Mistakes
- Treating read-only transactions as write protection. The read-only flag is a hint, not a general guarantee that writes are forbidden.
- Assuming save means an immediate SQL statement. Spring Data JPA delegates saving to
EntityManager.persistormerge, depending on whether it considers the entity new. SQL timing also depends on flushing and identifier generation. - Copying demo schema settings into production. Keep schema changes in controlled migrations; the database initialization guide explains the available mechanisms.
Spring Data JPA Tutorial
Build your understanding in this order: entity mappings, repository CRUD, query methods, transaction boundaries, then relationship fetching. Use the Spring Data JPA tutorial for a longer learning path and the Spring Data JPA one-to-many example to explore relationships. At each step, inspect the SQL rather than judging correctness only from returned objects.
Spring Data JPA Interview Questions
Be ready to explain why repositories don't replace Hibernate, why a managed update can work without save, and why a single repository method can cause multiple SQL queries. Those explanations demonstrate more understanding than memorizing interface names. Use the Spring Data JPA interview questions to practice.
FAQ
What is the difference between Spring Data JPA, Jakarta Persistence, and Hibernate?
Spring Data JPA supplies repository implementations, Jakarta Persistence defines the persistence contract, and Hibernate is a provider implementing that contract. Spring Data JPA does not replace Hibernate; they commonly work together.
What is the difference between CrudRepository and JpaRepository?
CrudRepository provides generic CRUD operations. JpaRepository adds paging, sorting, list-returning collection methods, and JPA-specific operations such as flush(), making it a broader contract for JPA applications.
How does Spring Data JPA derive a query from a method name?
Spring Data parses supported method-name patterns and resolves their property paths against the entity model. For example, findByUsername uses the username property; arbitrary method names do not automatically become valid queries.
When should I use @Query instead of a derived query method?
Use @Query when a derived name becomes difficult to read or cannot express the query you need. A JPQL query refers to entity names and attributes, and named parameters can be bound with @Param.
What does the javax.persistence to jakarta.persistence change mean?
Jakarta Persistence 3.0 moved the persistence APIs to the jakarta.persistence namespace. When upgrading an older application, align the framework, provider, API dependencies, and entity imports rather than mixing javax.persistence and jakarta.persistence types.
Further Reading
- Spring's Accessing Data with JPA guide — a guided repository application.
- Jakarta Persistence specification — entity mappings, persistence contexts, and query semantics.
- Spring Data repository core concepts — CRUD, list-returning interfaces, and paging.
- JPA query methods — derivation, explicit queries, parameters, and entity graphs.
- Repository transaction behavior — defaults and service-layer boundaries.
- Persisting entities — how save chooses persist or merge.
- Hibernate introduction — entity lifecycle, SQL inspection, and fetching.
- Spring Boot database initialization — schema generation and migration choices.
