00:00

Spring Data JPA Overview

Spring Data JPA is part of the larger Spring Data family. It makes it much easier to work with relational databases in Spring applications by cutting out most of the repetitive code needed to save, read, update and delete data.

This guide explains how Spring Data JPA relates to JPA and Hibernate, then builds a complete example step by step: configuration, entity, repository, derived and custom queries, pagination, projections, transactions, auditing, a REST controller and repository tests, with formatted Java code throughout.

1. What Is Spring Data JPA?

Without Spring Data JPA, developers usually write a DAO (Data Access Object) class for every entity and fill it with the same kind of code again and again: opening an EntityManager, writing queries, handling transactions and mapping results. Spring Data JPA removes this boilerplate. You simply create an interface, and Spring builds the implementation for you at runtime.

Without Spring Data JPA: a hand-written DAO
@Repository
public class EmployeeDao {

    @PersistenceContext
    private EntityManager entityManager;

    public Employee findById(Long id) {
        return entityManager.find(Employee.class, id);
    }

    public List<Employee> findByDepartment(String department) {
        return entityManager
                .createQuery("SELECT e FROM Employee e WHERE e.department = :dept", Employee.class)
                .setParameter("dept", department)
                .getResultList();
    }

    @Transactional
    public Employee save(Employee employee) {
        entityManager.persist(employee);
        return employee;
    }
}
With Spring Data JPA: just an interface
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    List<Employee> findByDepartment(String department);
}

The interface gives you findById(), save() and many other methods for free, and findByDepartment() is turned into a query from its name.

2. How It Fits with JPA and Hibernate

It helps to understand the three layers involved:

The persistence layers
  Your code (services, controllers)
          |
          v
  Spring Data JPA      repositories, derived queries, paging, auditing
          |
          v
  JPA (Jakarta Persistence API)   the standard: @Entity, EntityManager, JPQL
          |
          v
  Hibernate            the implementation that generates SQL
          |
          v
  JDBC driver  ->  Database (MySQL, PostgreSQL, H2 ...)

JPA is a specification: a set of rules and interfaces for mapping Java objects to database tables. Hibernate is the most popular implementation of JPA and does the actual work of talking to the database. Spring Data JPA sits on top of both and adds a simpler, repository-based way of using them.

In short, Spring Data JPA does not replace JPA or Hibernate. It uses them underneath and makes them easier to work with.

3. Key Features

FeatureWhat it means
Ready-made repositoriesCommon CRUD methods such as save(), findById(), findAll() and deleteById() with no code
Derived queriesQueries created from method names, such as findByEmail(String email)
Custom queriesYour own JPQL or native SQL with the @Query annotation
Pagination and sortingBuilt-in Pageable, Page and Sort support
ProjectionsLoad only selected columns into interfaces or records
AuditingAutomatically track who created or changed a record and when
TransactionsRepository methods are transactional by default

4. Repository Interfaces

Spring Data JPA offers a family of repository interfaces, each adding more features:

InterfaceWhat it provides
RepositoryA marker interface with no methods; Spring still detects it
CrudRepositoryBasic create, read, update and delete operations; collection results are Iterable
ListCrudRepositoryThe same CRUD methods, but returning List instead of Iterable
PagingAndSortingRepositoryfindAll(Pageable) and findAll(Sort); since Spring Data 3 it no longer includes CRUD methods by itself
ListPagingAndSortingRepositorySorting methods that return List
JpaRepositoryAll of the above plus JPA-specific methods such as flush(), saveAndFlush(), getReferenceById() and batch deletes

In most projects, developers extend JpaRepository because it includes everything they usually need. If you want to expose fewer operations, for example a read-only repository, extend Repository and declare only the methods you want.

5. Setting Up the Project

Add the dependencies

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Database driver: choose the one for your database -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- In-memory database for tests -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

Configure the database

application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/company
    username: app_user
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate        # check tables match entities; use a migration tool to change them
    show-sql: true              # print SQL while learning; turn off in production
    open-in-view: false         # keep database access inside the service layer
    properties:
      hibernate:
        format_sql: true

Spring Boot creates the data source, the EntityManager and the transaction manager from these settings. For schema changes, use a migration tool such as Flyway or Liquibase instead of ddl-auto=update; see Spring Boot Database Migration.

6. Creating an Entity

An entity is a Java class mapped to a database table. Each object is one row:

Employee.java
package com.example.employees;

import java.math.BigDecimal;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "employees")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    private String department;

    private BigDecimal salary;

    protected Employee() {
        // required by JPA
    }

    public Employee(String name, String email, String department, BigDecimal salary) {
        this.name = name;
        this.email = email;
        this.department = department;
        this.salary = salary;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public String getDepartment() { return department; }
    public BigDecimal getSalary() { return salary; }

    public void setDepartment(String department) { this.department = department; }
    public void setSalary(BigDecimal salary) { this.salary = salary; }
}
AnnotationPurpose
@EntityMarks the class as a JPA entity
@TableSets the table name (default: the class name)
@IdMarks the primary key
@GeneratedValueLets the database generate the key; IDENTITY uses an auto-increment column
@ColumnCustomizes a column, such as length, nullability or uniqueness

JPA needs a no-argument constructor; making it protected stops other code from creating empty employees by accident.

7. Creating a Repository Interface

EmployeeRepository.java
package com.example.employees;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    Optional<Employee> findByEmail(String email);

    List<Employee> findByDepartment(String department);

    @Query("SELECT e FROM Employee e WHERE e.name LIKE %:keyword%")
    List<Employee> searchByName(@Param("keyword") String keyword);
}

The two type parameters are the entity type (Employee) and the type of its ID (Long). There is no implementation class to write: Spring creates one automatically when the application starts, and you can inject it anywhere.

8. Built-in CRUD Methods

MethodWhat it does
save(entity)Inserts a new entity or updates an existing one, and returns the saved entity
saveAll(entities)Saves several entities at once
findById(id)Returns an Optional that is empty if no row exists
findAll()Returns all rows (avoid on large tables; use paging instead)
existsById(id)Checks whether a row exists
count()Counts all rows
deleteById(id) / delete(entity)Deletes a row
getReferenceById(id)Returns a lazy reference without loading the row immediately
Using the CRUD methods
Employee asha = employeeRepository.save(
        new Employee("Asha Verma", "asha@example.com", "IT", new BigDecimal("75000")));

Optional<Employee> found = employeeRepository.findById(asha.getId());
found.ifPresent(e -> System.out.println("Found " + e.getName()));

long total = employeeRepository.count();
employeeRepository.deleteById(asha.getId());

9. Derived Query Methods

Spring Data reads the method name, splits it into parts and builds a JPQL query. The part after findBy names entity properties, joined with keywords:

KeywordExample methodGenerated condition
And / OrfindByDepartmentAndSalaryGreaterThandepartment = ? AND salary > ?
BetweenfindBySalaryBetweensalary BETWEEN ? AND ?
LessThan / GreaterThanEqualfindBySalaryLessThansalary < ?
Containing + IgnoreCasefindByNameContainingIgnoreCaseUPPER(name) LIKE UPPER('%?%')
StartingWithfindByEmailStartingWithemail LIKE '?%'
InfindByDepartmentIndepartment IN (?)
IsNullfindByDepartmentIsNulldepartment IS NULL
OrderByfindByDepartmentOrderByNameAsc... ORDER BY name ASC
Top / FirstfindTop3ByOrderBySalaryDescFirst 3 rows by highest salary
countBy / existsBycountByDepartment, existsByEmailReturns a number or a boolean
Derived queries in the repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    List<Employee> findByDepartmentAndSalaryGreaterThan(String department, BigDecimal salary);

    List<Employee> findByNameContainingIgnoreCase(String text);

    List<Employee> findByDepartmentIn(Collection<String> departments);

    List<Employee> findTop3ByOrderBySalaryDesc();

    long countByDepartment(String department);

    boolean existsByEmail(String email);
}

Spring checks these methods at startup. If a name refers to a property that does not exist, the application fails to start with a clear error, so mistakes are found early. Keep derived names short; for complex conditions, use @Query.

10. Custom Queries with @Query

When a method name would become too long, write the query yourself in JPQL, which uses entity and field names instead of table and column names:

JPQL, native SQL and updates
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // JPQL with named parameters
    @Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary >= :minSalary")
    List<Employee> findWellPaid(@Param("dept") String department,
                                @Param("minSalary") BigDecimal minSalary);

    // Native SQL when you need database-specific features
    @Query(value = "SELECT * FROM employees WHERE email LIKE %:domain", nativeQuery = true)
    List<Employee> findByEmailDomain(@Param("domain") String domain);

    // Bulk update: needs @Modifying and a transaction
    @Modifying
    @Transactional
    @Query("UPDATE Employee e SET e.salary = e.salary * :factor WHERE e.department = :dept")
    int raiseSalaries(@Param("dept") String department, @Param("factor") BigDecimal factor);
}
OptionUse when
Derived querySimple conditions that read clearly as a method name
@Query with JPQLJoins, several conditions or calculated results, while staying database-independent
@Query with nativeQuery = trueDatabase-specific SQL features
@ModifyingUPDATE or DELETE statements that change many rows at once

Bulk updates go straight to the database and skip the entities already loaded in memory. Add @Modifying(clearAutomatically = true) if the same transaction reads those entities again afterwards.

11. Pagination and Sorting

Never load a whole large table at once. Use Pageable to read one page at a time, and Sort to order the results:

Paging in the repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    Page<Employee> findByDepartment(String department, Pageable pageable);
}
Requesting a page
Pageable firstPage = PageRequest.of(0, 10, Sort.by("name").ascending());   // page 0, 10 rows

Page<Employee> page = employeeRepository.findByDepartment("IT", firstPage);

System.out.println("Employees on this page: " + page.getNumberOfElements());
System.out.println("Total employees:        " + page.getTotalElements());
System.out.println("Total pages:            " + page.getTotalPages());

A Page runs an extra count query to know the total. If you only need “is there a next page?”, return Slice instead, which skips the count. See Spring Boot Pagination and Sorting for REST examples.

12. Projections: Reading Only What You Need

Often you only need a few columns, for example names and emails for a list screen. A projection loads just those fields, which is faster than loading full entities:

EmployeeSummary.java
package com.example.employees;

public interface EmployeeSummary {

    String getName();

    String getEmail();
}
Projection query in the repository
// Text between "find" and "By" is ignored, so this is a normal derived query
List<EmployeeSummary> findSummaryByDepartment(String department);

Spring Data selects only the name and email columns and returns lightweight objects. You can also use a Java record with matching component names as the return type. Projections are a good fit for API responses, because they avoid exposing whole entities.

13. Transactions

Repository methods already run in transactions: read methods use read-only transactions and save or delete methods use read-write ones. When one business operation calls the repository several times, put @Transactional on the service method so all steps succeed or fail together:

Transferring an employee between departments
@Service
public class EmployeeTransferService {

    private final EmployeeRepository employeeRepository;

    public EmployeeTransferService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Transactional
    public void transfer(Long employeeId, String newDepartment, BigDecimal newSalary) {
        Employee employee = employeeRepository.findById(employeeId)
                .orElseThrow(() -> new IllegalArgumentException("Employee not found: " + employeeId));

        employee.setDepartment(newDepartment);
        employee.setSalary(newSalary);
        // no save() needed: changes to a loaded entity are written at commit
    }

    @Transactional(readOnly = true)
    public List<Employee> listDepartment(String department) {
        return employeeRepository.findByDepartment(department);
    }
}

Inside a transaction, Hibernate tracks the entities it loaded and writes any changes automatically when the transaction commits. This is called dirty checking. See Spring Boot Transaction Management for more.

14. Auditing: Created and Updated Timestamps

Spring Data JPA can fill in creation and update timestamps automatically. Enable auditing once:

JpaAuditingConfig.java
package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
Audit fields in the entity
@Entity
@Table(name = "employees")
@EntityListeners(AuditingEntityListener.class)
public class Employee {

    // ... id, name, email, department, salary

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    private Instant updatedAt;
}

Now every insert sets createdAt, and every update sets updatedAt. To record who made the change, add @CreatedBy and @LastModifiedBy fields and an AuditorAware bean that returns the current user.

15. Using the Repository in a Service and REST Controller

EmployeeService.java
package com.example.employees;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Transactional
    public Employee saveEmployee(Employee employee) {
        if (employeeRepository.existsByEmail(employee.getEmail())) {
            throw new IllegalStateException("Email already in use: " + employee.getEmail());
        }
        return employeeRepository.save(employee);
    }

    @Transactional(readOnly = true)
    public Employee getEmployee(Long id) {
        return employeeRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Employee not found: " + id));
    }

    @Transactional(readOnly = true)
    public List<EmployeeSummary> getByDepartment(String department) {
        return employeeRepository.findSummaryByDepartment(department);
    }
}
EmployeeController.java
package com.example.employees;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Employee create(@RequestBody Employee employee) {
        return employeeService.saveEmployee(employee);
    }

    @GetMapping("/{id}")
    public Employee get(@PathVariable Long id) {
        return employeeService.getEmployee(id);
    }

    @GetMapping
    public List<EmployeeSummary> byDepartment(@RequestParam String department) {
        return employeeService.getByDepartment(department);
    }
}

The controller handles HTTP, the service holds business rules and transactions, and the repository talks to the database. For real APIs, accept and return separate request and response classes (DTOs) instead of the entity, and add validation; see Spring Boot REST API.

16. Testing Repositories with @DataJpaTest

@DataJpaTest starts only the JPA part of the application with an in-memory database, so repository tests are fast. Each test runs in a transaction that is rolled back afterwards:

EmployeeRepositoryTest.java
package com.example.employees;

import static org.assertj.core.api.Assertions.assertThat;

import java.math.BigDecimal;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
class EmployeeRepositoryTest {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Test
    void findsEmployeeByEmail() {
        employeeRepository.save(
                new Employee("Asha Verma", "asha@example.com", "IT", new BigDecimal("75000")));

        assertThat(employeeRepository.findByEmail("asha@example.com"))
                .isPresent()
                .get()
                .extracting(Employee::getName)
                .isEqualTo("Asha Verma");
    }

    @Test
    void countsEmployeesPerDepartment() {
        employeeRepository.save(new Employee("Ravi", "ravi@example.com", "IT", new BigDecimal("60000")));
        employeeRepository.save(new Employee("Meera", "meera@example.com", "HR", new BigDecimal("55000")));

        assertThat(employeeRepository.countByDepartment("IT")).isEqualTo(1);
    }
}

This import is for Spring Boot 3; Spring Boot 4 moved the test annotation to a separate module. To test against the same database you use in production, combine @DataJpaTest with Testcontainers. See Spring Boot Testing.

17. Common Mistakes and Best Practices

MistakeProblemBetter approach
Loading related data in a loopThe N+1 query problem: one query for the list plus one per rowFetch what you need with a join fetch, @EntityGraph or a projection
ddl-auto=update in productionUnpredictable schema changes and no historyvalidate plus Flyway or Liquibase migrations
Returning entities from REST APIsExposes internal fields and can trigger lazy-loading errorsReturn DTOs or projections
findAll() on large tablesLoads everything into memoryUse Pageable or a filtered query
FetchType.EAGER everywhereLoads far more data than neededKeep associations lazy and fetch on demand
Leaving open-in-view onDatabase queries can run while rendering the responseSet spring.jpa.open-in-view=false
Very long derived method namesHard to read and maintainSwitch to @Query
  • Keep repositories small and focused on one entity (or aggregate).
  • Put @Transactional on service methods that change data in several steps.
  • Turn on show-sql while developing to see the SQL that Hibernate generates.
  • Test custom queries with @DataJpaTest.

18. Why Use Spring Data JPA?

The biggest benefit is less code. You focus on your business logic instead of writing the same database code over and over. Your data layer becomes cleaner and more consistent across the whole project, and features like pagination, sorting, projections and auditing come built in. Because it works on top of JPA, you can still drop down to custom queries or the EntityManager whenever you need more control.

19. Conclusion

Spring Data JPA is the standard way to handle database access in modern Spring Boot applications. By combining the power of JPA and Hibernate with simple repository interfaces, it lets you build a complete data layer with very little code: entities map your tables, repositories provide CRUD and derived queries, @Query covers custom needs, and paging, projections, transactions and auditing are built in. This makes your applications faster to develop and easier to maintain.