Full Stack • Java • System Design • Cloud • AI Engineering

Spring Data JPA Repository

Complete guide to Spring Data JPA Repository with JpaRepository, CrudRepository, query methods, JPQL, native queries, pagination, sorting, projections, specifications, examples, diagrams, and interview questions.

What is Spring Data JPA Repository?

Spring Data JPA Repository is an abstraction that reduces boilerplate database code.

Instead of writing DAO code manually:

entityManager.persist(employee);
entityManager.find(Employee.class, id);
entityManager.remove(employee);

Spring Data JPA allows:

employeeRepository.save(employee);
employeeRepository.findById(id);
employeeRepository.deleteById(id);

Simple meaning:

Repository = Interface that provides database operations

Why Repository is Important

Without Spring Data JPA:

@Repository
public class EmployeeDao {

    @PersistenceContext
    private EntityManager entityManager;

    public void save(Employee employee) {
        entityManager.persist(employee);
    }

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

    public void delete(Employee employee) {
        entityManager.remove(employee);
    }
}

With Spring Data JPA:

public interface EmployeeRepository
        extends JpaRepository<Employee, Long> {
}

Spring automatically creates implementation at runtime.


High Level Architecture

flowchart TD

A["Controller"]
B["Service"]
C["Repository Interface"]
D["Spring Data Proxy Implementation"]
E["EntityManager"]
F["Hibernate"]
G["Database"]

A --> B
B --> C
C --> D
D --> E
E --> F
F --> G

Repository Hierarchy

flowchart TD

A["Repository"]
B["CrudRepository"]
C["PagingAndSortingRepository"]
D["JpaRepository"]

A --> B
B --> C
C --> D

Main Repository Types

Repository Purpose
Repository Marker interface
CrudRepository Basic CRUD
PagingAndSortingRepository CRUD plus pagination and sorting
JpaRepository Full JPA repository features

CrudRepository

Provides basic CRUD methods.

public interface EmployeeRepository
        extends CrudRepository<Employee, Long> {
}

Common methods:

save(entity)
findById(id)
findAll()
delete(entity)
deleteById(id)
existsById(id)
count()

JpaRepository

Most commonly used in Spring Boot applications.

public interface EmployeeRepository
        extends JpaRepository<Employee, Long> {
}

JpaRepository includes:

CRUD
Pagination
Sorting
Batch delete
Flush methods
JPA-specific operations

Sample Entity

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;

    private String name;

    private String department;

    private Double salary;

    private String status;

    public Employee() {
    }

    public Employee(String name, String department, Double salary, String status) {
        this.name = name;
        this.department = department;
        this.salary = salary;
        this.status = status;
    }

    // getters and setters
}

Basic Repository

import org.springframework.data.jpa.repository.JpaRepository;

public interface EmployeeRepository
        extends JpaRepository<Employee, Long> {
}

That single interface gives many methods automatically.


Basic CRUD Service

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

import java.util.List;

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

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

    @Transactional
    public Employee createEmployee(Employee employee) {
        return employeeRepository.save(employee);
    }

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

    @Transactional(readOnly = true)
    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Transactional
    public Employee updateSalary(Long id, Double salary) {
        Employee employee = employeeRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Employee not found"));

        employee.setSalary(salary);

        return employee;
    }

    @Transactional
    public void deleteEmployee(Long id) {
        employeeRepository.deleteById(id);
    }
}

Important:

updateSalary does not call save.
Because employee is managed.
Dirty checking updates DB.

CRUD Flow Diagram

flowchart TD

A["Client Request"]
B["Controller"]
C["Service"]
D["Repository"]
E["EntityManager"]
F["Hibernate"]
G["Database"]

A --> B
B --> C
C --> D
D --> E
E --> F
F --> G

save Method

save() is used for both insert and update.

employeeRepository.save(employee);

Internally:

New entity
   persist

Existing or detached entity
   merge

save Flow

flowchart TD

A["repository.save entity"]
B["Is Entity New"]
C["persist"]
D["merge"]
E["Insert SQL"]
F["Update SQL"]

A --> B
B -->|Yes| C
B -->|No| D
C --> E
D --> F

Example Insert

Employee employee =
        new Employee("Venu", "Engineering", 100000.0, "ACTIVE");

employeeRepository.save(employee);

Generated SQL:

INSERT INTO employees
(name, department, salary, status)
VALUES
('Venu', 'Engineering', 100000, 'ACTIVE');

Example Update

@Transactional
public Employee updateDepartment(Long id) {

    Employee employee = employeeRepository.findById(id)
            .orElseThrow();

    employee.setDepartment("Architecture");

    return employee;
}

Generated SQL during commit:

UPDATE employees
SET department = 'Architecture'
WHERE id = 1;

Query Methods

Spring Data JPA can generate queries from method names.

List<Employee> findByDepartment(String department);

Generated logic:

SELECT *
FROM employees
WHERE department = ?;

Common Query Methods

List<Employee> findByStatus(String status);

List<Employee> findByDepartmentAndStatus(
        String department,
        String status
);

List<Employee> findBySalaryGreaterThan(Double salary);

List<Employee> findByNameContaining(String keyword);

List<Employee> findByDepartmentOrderBySalaryDesc(String department);

boolean existsByName(String name);

long countByDepartment(String department);

void deleteByStatus(String status);

Query Method Diagram

flowchart LR

A["Method Name"]
B["Spring Data Parser"]
C["Generate JPQL"]
D["Execute SQL"]

A --> B
B --> C
C --> D

Query Method Keywords

Keyword Example
And findByDepartmentAndStatus
Or findByDepartmentOrStatus
GreaterThan findBySalaryGreaterThan
LessThan findBySalaryLessThan
Between findBySalaryBetween
Like findByNameLike
Containing findByNameContaining
StartingWith findByNameStartingWith
EndingWith findByNameEndingWith
In findByDepartmentIn
OrderBy findByDepartmentOrderBySalaryDesc

JPQL Query using @Query

Use JPQL when method names become too long.

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

public interface EmployeeRepository
        extends JpaRepository<Employee, Long> {

    @Query("select e from Employee e where e.department = :department")
    List<Employee> findEmployeesByDepartment(
            @Param("department") String department
    );
}

JPQL uses entity names and fields, not table names.


JPQL with Multiple Conditions

@Query("""
       select e
       from Employee e
       where e.department = :department
       and e.salary >= :minSalary
       and e.status = :status
       """)
List<Employee> searchEmployees(
        @Param("department") String department,
        @Param("minSalary") Double minSalary,
        @Param("status") String status
);

Native Query

Use native query when you need database-specific SQL.

@Query(
    value = """
            SELECT *
            FROM employees
            WHERE department = :department
            """,
    nativeQuery = true
)
List<Employee> findByDepartmentNative(
        @Param("department") String department
);

JPQL vs Native Query

| Feature | JPQL | Native SQL | |---|---| | Uses | Entity fields | Table columns | | Portable | Yes | Less | | DB-specific features | Limited | Full | | Refactoring safety | Better | Lower | | Recommended | Most cases | Special cases |


Pagination

Use Pageable.

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

Page<Employee> findByDepartment(String department, Pageable pageable);

Service:

@Transactional(readOnly = true)
public Page<Employee> getEmployeesByDepartment(
        String department,
        int page,
        int size
) {
    Pageable pageable =
            PageRequest.of(page, size);

    return employeeRepository.findByDepartment(department, pageable);
}

Generated SQL:

SELECT *
FROM employees
WHERE department = ?
LIMIT ? OFFSET ?;

Pagination Flow

flowchart LR

A["Client page and size"]
B["PageRequest"]
C["Repository"]
D["Limit Offset Query"]
E["Page Result"]

A --> B
B --> C
C --> D
D --> E

Sorting

List<Employee> employees =
        employeeRepository.findAll(
                Sort.by(Sort.Direction.DESC, "salary")
        );

Generated SQL:

SELECT *
FROM employees
ORDER BY salary DESC;

Pagination with Sorting

Pageable pageable =
        PageRequest.of(
                0,
                10,
                Sort.by(Sort.Direction.DESC, "salary")
        );

Page<Employee> page =
        employeeRepository.findByDepartment("Engineering", pageable);

Projection

Projection allows fetching only required fields.

Instead of loading full entity:

public interface EmployeeNameSalaryProjection {

    String getName();

    Double getSalary();
}

Repository:

List<EmployeeNameSalaryProjection>
findByDepartment(String department);

Usage:

List<EmployeeNameSalaryProjection> result =
        employeeRepository.findByDepartment("Engineering");

for (EmployeeNameSalaryProjection employee : result) {
    System.out.println(employee.getName());
    System.out.println(employee.getSalary());
}

Projection Diagram

flowchart TD

A["Query Employee"]
B["Need Only Name Salary"]
C["Projection Interface"]
D["Fetch Selected Columns"]
E["Less Memory"]

A --> B
B --> C
C --> D
D --> E

DTO Projection

public record EmployeeSummaryDto(
        String name,
        String department,
        Double salary
) {
}

Repository:

@Query("""
       select new com.codewithvenu.dto.EmployeeSummaryDto(
           e.name,
           e.department,
           e.salary
       )
       from Employee e
       where e.department = :department
       """)
List<EmployeeSummaryDto> findEmployeeSummary(
        @Param("department") String department
);

Modifying Query

Use @Modifying for update or delete JPQL queries.

import org.springframework.data.jpa.repository.Modifying;

@Modifying
@Query("""
       update Employee e
       set e.status = :status
       where e.department = :department
       """)
int updateStatusByDepartment(
        @Param("department") String department,
        @Param("status") String status
);

Service:

@Transactional
public int deactivateDepartment(String department) {
    return employeeRepository.updateStatusByDepartment(
            department,
            "INACTIVE"
    );
}

Important:

Bulk update bypasses persistence context.
Use clearAutomatically if needed.

Better:

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
       update Employee e
       set e.status = :status
       where e.department = :department
       """)
int updateStatusByDepartment(
        @Param("department") String department,
        @Param("status") String status
);

Bulk Update Problem Diagram

flowchart TD

A["Persistence Context Has Employee ACTIVE"]
B["Bulk Update Sets INACTIVE"]
C["Database Updated"]
D["Persistence Context Still ACTIVE"]
E["clearAutomatically"]
F["Fresh Data Next Time"]

A --> B
B --> C
C --> D
D --> E
E --> F

@Lock with Repository

import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.Lock;

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select e from Employee e where e.id = :id")
Optional<Employee> findByIdForUpdate(@Param("id") Long id);

Use for high-conflict updates.


@EntityGraph to Avoid N+1 Problem

Suppose Employee has Department.

@Entity
public class Employee {

    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;
}

Repository:

import org.springframework.data.jpa.repository.EntityGraph;

@EntityGraph(attributePaths = {"department"})
List<Employee> findByStatus(String status);

This fetches employee and department together.


EntityGraph Flow

flowchart TD

A["Repository Method"]
B["@EntityGraph department"]
C["Fetch Employee"]
D["Fetch Department Together"]
E["Avoid N Plus One"]

A --> B
B --> C
C --> D
D --> E

Specifications

Specifications help build dynamic queries.

Repository:

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface EmployeeRepository
        extends JpaRepository<Employee, Long>,
                JpaSpecificationExecutor<Employee> {
}

Specification:

public class EmployeeSpecification {

    public static Specification<Employee> hasDepartment(String department) {
        return (root, query, criteriaBuilder) ->
                criteriaBuilder.equal(root.get("department"), department);
    }

    public static Specification<Employee> salaryGreaterThan(Double salary) {
        return (root, query, criteriaBuilder) ->
                criteriaBuilder.greaterThan(root.get("salary"), salary);
    }

    public static Specification<Employee> hasStatus(String status) {
        return (root, query, criteriaBuilder) ->
                criteriaBuilder.equal(root.get("status"), status);
    }
}

Service:

@Transactional(readOnly = true)
public List<Employee> search(
        String department,
        Double minSalary,
        String status
) {
    Specification<Employee> spec =
            Specification.where(EmployeeSpecification.hasDepartment(department))
                    .and(EmployeeSpecification.salaryGreaterThan(minSalary))
                    .and(EmployeeSpecification.hasStatus(status));

    return employeeRepository.findAll(spec);
}

Specification Diagram

flowchart LR

A["Search Filters"]
B["Specification 1"]
C["Specification 2"]
D["Specification 3"]
E["Combined Predicate"]
F["Dynamic Query"]

A --> B
A --> C
A --> D
B --> E
C --> E
D --> E
E --> F

Custom Repository

Use custom repository when Spring Data method names are not enough.

Interface:

public interface EmployeeCustomRepository {

    List<Employee> searchEmployees(
            String department,
            Double minSalary,
            String status
    );
}

Implementation:

@Repository
public class EmployeeCustomRepositoryImpl
        implements EmployeeCustomRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Employee> searchEmployees(
            String department,
            Double minSalary,
            String status
    ) {
        String jpql = """
                select e
                from Employee e
                where (:department is null or e.department = :department)
                and (:minSalary is null or e.salary >= :minSalary)
                and (:status is null or e.status = :status)
                """;

        return entityManager.createQuery(jpql, Employee.class)
                .setParameter("department", department)
                .setParameter("minSalary", minSalary)
                .setParameter("status", status)
                .getResultList();
    }
}

Main Repository:

public interface EmployeeRepository
        extends JpaRepository<Employee, Long>,
                EmployeeCustomRepository {
}

Custom Repository Diagram

flowchart TD

A["EmployeeRepository"]
B["JpaRepository Methods"]
C["Custom Repository Methods"]
D["EntityManager Implementation"]

A --> B
A --> C
C --> D

REST Controller Example

import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;

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

    private final EmployeeService employeeService;

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

    @PostMapping
    public Employee create(@RequestBody Employee employee) {
        return employeeService.createEmployee(employee);
    }

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

    @GetMapping
    public Page<Employee> search(
            @RequestParam String department,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size
    ) {
        return employeeService.getEmployeesByDepartment(
                department,
                page,
                size
        );
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        employeeService.deleteEmployee(id);
    }
}

Repository Best Practices

✅ Use JpaRepository for most applications

✅ Keep business logic in service layer

✅ Use query methods for simple filters

✅ Use @Query for complex fixed queries

✅ Use Specifications for dynamic filters

✅ Use projections for read-only API responses

✅ Use pagination for large result sets

✅ Use @EntityGraph to avoid N+1 queries

✅ Use @Modifying carefully with transactions


Common Mistakes

❌ Writing very long method names

findByDepartmentAndStatusAndSalaryGreaterThanAndNameContaining...

Use @Query or Specification instead.

❌ Returning List for huge data

Use pagination.

❌ Updating entities without transaction

Use @Transactional.

❌ Using entity directly as API response in large apps

Use DTOs.

❌ Forgetting bulk updates bypass persistence context

Use clearAutomatically.


Interview Questions

Q1. What is Spring Data JPA Repository?

It is an abstraction that provides database operations using repository interfaces.


Q2. Difference between CrudRepository and JpaRepository?

CrudRepository provides basic CRUD.
JpaRepository provides CRUD plus pagination, sorting, flushing, and JPA-specific methods.

Q3. Does Spring create repository implementation manually?

No.

Spring generates proxy implementation at runtime.


Q4. What is query method?

A method where Spring generates query based on method name.

Example:

findByDepartmentAndStatus

Q5. Difference between JPQL and native query?

JPQL uses entity names and fields.
Native query uses database tables and columns.

Q6. Why use projections?

To fetch only required fields and improve performance.


Q7. What is @Modifying?

Used for update and delete JPQL queries.


Q8. Why use @EntityGraph?

To fetch related entities eagerly for a specific query and avoid N+1 problem.


Summary

Spring Data JPA Repository reduces boilerplate code and makes database access easier.

Main idea:

Define interface
      ↓
Extend JpaRepository
      ↓
Spring creates implementation
      ↓
Use methods directly

Most used tools:

JpaRepository
Query Methods
@Query
Pagination
Sorting
Projection
@Modifying
@EntityGraph
Specification
Custom Repository

Best rule:

Repository = Database access
Service = Business logic
Controller = API layer