Repository Methods (Derived Queries)
How Spring Data parses method names into queries, prefixes like findBy, existsBy, countBy and deleteBy, Top and First, Distinct, IgnoreCase, And and Or, supported return types, and the pitfalls of derived queries.
#Introduction
A large share of everyday database access in Spring applications is written as repository method names instead of JPQL or SQL. Spring Data reads the method name and builds the query for you.
Instead of writing:
SELECT * FROM employee WHERE department = 'IT';you declare:
List<Employee> findByDepartment(String department);#How It Works
findByDepartment is split into find (the subject), By (the separator) and Department (the criteria) → select e from Employee e where e.department = ?1.
#The Sample Entity
@Entity
public class Employee {
@Id
@GeneratedValue
private Integer id;
private String name;
private String email;
private String department;
private BigDecimal salary;
private Integer age;
private Boolean active;
private LocalDate joiningDate;
}#Naming Rules
<prefix>[Distinct|TopN|FirstN]By<Property><Keyword>[And|Or<Property><Keyword>…][OrderBy<Property>Asc|Desc]findByName(String name)
findBySalaryGreaterThan(BigDecimal salary)
findByAgeBetween(Integer min, Integer max)
findByDepartmentAndSalary(String dept, BigDecimal salary)
findByNameIgnoreCase(String name)#Prefixes
| Prefix | Purpose |
|---|---|
findBy, readBy, getBy, queryBy, searchBy, streamBy | Retrieve data — functionally equivalent |
existsBy | boolean — does any row match? |
countBy | long — how many rows match? |
deleteBy, removeBy | Delete matching entities |
#findBy
List<Employee> findByDepartment(String department);List<Employee> list = repository.findByDepartment("IT");For a collection return type, no matching rows → an empty list — never null. One row gives a list with one element.
#findById and Optional
Optional<Employee> emp = repository.findById(10); // built into CrudRepository
emp.ifPresent(System.out::println);
Employee e = repository.findById(10).orElseThrow(() -> new EmployeeNotFoundException(10));Optional makes "the row might not exist" explicit.
Single-Result Methods
Optional<Employee> findByEmail(String email); // ✅ preferred
Employee findByEmail(String email); // returns null if nothing matches#existsBy
boolean existsByEmail(String email);if (repository.existsByEmail(email)) {
throw new DuplicateEmailException(email);
}Spring generates an efficient existence check (it selects a single row with a limit) rather than loading entities. Used for registration, duplicate checks and validation.
#countBy
long countByDepartment(String department);select count(e1_0.id) from employee e1_0 where e1_0.department=?#deleteBy and removeBy
@Transactional
long deleteByDepartment(String department); // returns how many were deletedremoveBy… is identical to deleteBy….
#Top and First
Optional<Employee> findTopByDepartment(String department); // LIMIT 1
Optional<Employee> findFirstByOrderByJoiningDateAsc(); // earliest joiner
Optional<Employee> findTopByOrderBySalaryDesc(); // highest paid
List<Employee> findTop3ByOrderBySalaryDesc(); // top 3 salaries
List<Employee> findFirst5ByOrderByAgeAsc(); // 5 youngestselect … from employee e1_0 order by e1_0.salary desc fetch first ? rows onlyTop and First are interchangeable. Without an OrderBy, "first" is whatever the database returns first — always add ordering.
#Distinct
List<Employee> findDistinctByDepartment(String department);Produces select distinct e from Employee e where …. Most useful when joining collections (which can return duplicate parent rows).
#IgnoreCase
List<Employee> findByNameIgnoreCase(String name);
List<Employee> findByDepartmentAllIgnoreCase(String department); // AllIgnoreCase = every String criterionSpring generates upper(e.name) = upper(?), matching John, JOHN, john and JoHn.
#And / Or
List<Employee> findByDepartmentAndSalary(String dept, BigDecimal salary); // WHERE department=? AND salary=?
List<Employee> findByDepartmentOrSalary(String dept, BigDecimal salary); // WHERE department=? OR salary=?
List<Employee> findByDepartmentAndSalaryAndActive(String dept, BigDecimal salary, Boolean active);Parameters are bound in the order they appear in the method name.
#Return Types
| Return type | When |
|---|---|
Employee / Optional<Employee> | At most one result |
List<Employee>, Set<Employee>, Collection<Employee> | Many results |
Page<Employee> / Slice<Employee> | With a Pageable parameter (see Pagination & Sorting) |
Stream<Employee> | Process large results incrementally — must run in a transaction and be closed (try-with-resources) |
| A projection interface, record or DTO | Only some columns (see Projections, Specifications & Auditing) |
long, boolean | countBy, existsBy |
@Transactional(readOnly = true)
public void export() {
try (Stream<Employee> stream = repository.streamAllByActiveTrue()) {
stream.forEach(csvWriter::write);
}
}#Property Names Must Match the Entity
private String department; // entity field
findByDepartment(...) // ✅
findByDept(...) // ❌ startup errorNo property 'dept' found for type 'Employee'Errors appear at startup, not at runtime — a nice safety net.
#Interview Questions
Q1How does Spring Data JPA generate repository methods?
At startup it creates a proxy for each repository interface, parses each method name into a query (validating property names), and at runtime executes it through the JPA provider (Hibernate), which generates the SQL.
Q2What happens if a method references a non-existent property?
The application fails to start with an exception saying no property with that name exists on the entity.
Q3What's the difference between findBy, readBy and getBy?
For derived queries they're functionally equivalent — it's a naming preference.
Q4findTopBy vs findFirstBy?
They're the same; both limit the results, and accept a number (findTop3By…) and ordering.
Q5How does a derived deleteBy method work?
It loads the matching entities and removes them one by one (so cascades and callbacks run), returning the count — it isn't a single bulk DELETE. It must run in a transaction.
Q6When should you avoid derived query methods?
When names become long and hard to read, or you need joins across several entities, aggregations, subqueries or database-specific SQL — use @Query, native queries or Specifications instead.
#Best Practices
- Use derived methods for simple lookups.
- Return
Optional<T>for at most one result. - Use
Page/SlicewithPageablefor large results. - Switch to
@Queryor Specifications when names get unwieldy. - Back
existsBychecks with unique constraints; use bulk@Modifyingqueries for mass deletes.
#Key Takeaways
- Spring Data builds queries from method names: prefix +
By+ property + keyword. - Collection results are never null; single results can be
Optional. existsByandcountByare efficient; deriveddeleteBydeletes entity by entity.Top/Firstlimit results — combine withOrderBy.- Property names are validated at startup.