Derived Query Methods.
Hook: Interviewers love this because it shows whether you can let Spring Data write the query for you without losing control of the database behavior.
Question: What are derived query methods in Spring Data JPA?
Answer: Derived query methods are repository methods whose names Spring Data JPA reads and turns into database queries. For example, findByLastName, countByActiveTrue, or findTop3ByOrderByCreatedAtDesc all work without writing SQL or JPQL. They are great for simple lookups because they are quick to write and easy to read, and invalid property names usually fail fast when the repository starts.
Interview-Ready Answer: In Spring Data JPA, derived query methods are repository methods where the method name itself describes the query. I can write something like findByEmail or findByLastNameAndActiveTrue, and Spring parses that name, validates the entity fields, and generates the query automatically. I use them for simple, readable queries, but if the logic gets complex or I need more control, I switch to @Query or Specifications.
What it is
Derived queries are also called query derivation: you express the intent in the repository method name, and Spring Data builds the query for you. The name is made of a verb-like prefix such as find, count, exists, or delete, followed by By, then the filter conditions.
How it works under the hood
- Spring creates a proxy (a stand-in object) for your repository interface so your code can call methods as if they were normal Java methods.
- For each method, Spring Data uses a query lookup strategy to decide whether it should use an annotated query, a named query, or a derived query.
- For derived queries, Spring parses the method name with an internal parser called
PartTree. That parser breaks the name into parts likeAnd,Or,Between,IgnoreCase,OrderBy,Top, andFirst. - Spring checks the property path against your entity model. If you wrote
findByEmaiilbut the entity field isemail, the application usually fails fast with aPropertyReferenceException. - Spring generates the JPQL or SQL equivalent and caches the query metadata so later calls do not re-parse the method name every time.
- When you call the method, Spring binds the method arguments, executes the query, and maps the result into the declared return type such as
List,Optional,boolean, orPage.
When to use it
Use derived queries when the query is short, stable, and maps directly to entity fields. Good examples are lookup-by-email, filter-by-status, existence checks, count queries, and simple ordered lists. If the method starts reading like a paragraph, that is your cue to switch to @Query or Specifications.
Derived query vs other options
| Option | Best for | Trade-off |
|---|---|---|
| Derived query | Simple lookups | Very readable until it gets long |
@Query | Complex joins | You write the query yourself |
| Specifications | Dynamic filters | More code, more flexible |
Performance and edge cases
The parsing cost is tiny and happens at startup or repository creation time; think O(n) in method-name length, usually microseconds per method. After that, the important cost is the database round trip, which is often 1-10 ms on a healthy system, so derived queries are usually not slower than handwritten JPQL in any meaningful way.
- Single-result methods: If you return
Optional<Customer>or a single entity and the database returns two rows, Spring throwsIncorrectResultSizeDataAccessException. - Ordering:
findTopBy...orfindFirstBy...withoutOrderByis not deterministic; the database can pick any matching row. - Case handling:
IgnoreCaseis for string comparisons. It does not magically make every type case-insensitive. - Long names: The runtime is fine, but unreadable names become a maintenance problem fast.
Real-World Example: Imagine a checkout service in an e-commerce app. The team uses findByEmail to load the customer account and findTop3ByActiveTrueOrderByCreatedAtDesc to show recent active customers in an admin dashboard. Everything is simple and fast to read, which is exactly why derived queries are popular in production.
Now the bug: an old data migration accidentally created two rows with the same email address. The repository method still compiled and the app still started, but the first request that called findByEmail crashed with IncorrectResultSizeDataAccessException. Users saw 500 errors during login or checkout, and the logs showed a message like query did not return a unique result.
That is the key lesson: derived queries do not replace data rules. If a method is supposed to return one row, the database should also enforce uniqueness with a unique constraint, otherwise Spring can only detect the problem at runtime.
package com.example.demo;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
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.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.jpa.repository.JpaRepository;
@SpringBootApplication
public class DerivedQueryMethodsApplication {
public static void main(String[] args) {
SpringApplication.run(DerivedQueryMethodsApplication.class, args);
}
@Bean
CommandLineRunner demo(CustomerRepository repo) {
return args -> {
repo.saveAllAndFlush(List.of(
new Customer("Ava", "Patel", "ava@example.com", true,
LocalDateTime.now().minusDays(2), new Address("Boston", "1 Main St")),
new Customer("Noah", "Patel", "noah@example.com", false,
LocalDateTime.now().minusDays(1), new Address("Boston", "2 Main St")),
new Customer("Mia", "Chen", "mia@example.com", true,
LocalDateTime.now().minusHours(5), new Address("Austin", "3 Main St")),
new Customer("Ivy", "Chen", "dup@example.com", true,
LocalDateTime.now().minusHours(2), new Address("Austin", "4 Main St")),
new Customer("Ethan", "Stone", "dup@example.com", false,
LocalDateTime.now().minusHours(1), new Address("Dallas", "5 Main St"))
));
System.out.println("=== derived queries ===");
System.out.println("Patel customers: " + repo.findByLastNameIgnoreCase("patel"));
System.out.println("Austin customers: " + repo.findByAddressCity("Austin"));
System.out.println("Active Patels: " + repo.findByLastNameAndActiveTrueOrderByCreatedAtDesc("Patel"));
System.out.println("Top active customers: " + repo.findTop3ByActiveTrueOrderByCreatedAtDesc());
System.out.println("Active count: " + repo.countByActiveTrue());
System.out.println("Ava exists? " + repo.existsByEmail("ava@example.com"));
System.out.println("Missing email Optional: " + repo.findByEmail("missing@example.com"));
try {
// This fails because the method expects one row, but the database has two matches.
System.out.println("Duplicate email lookup: " + repo.findByEmail("dup@example.com"));
} catch (IncorrectResultSizeDataAccessException ex) {
String message = ex.getMostSpecificCause() != null ? ex.getMostSpecificCause().getMessage() : ex.getMessage();
System.out.println("Expected failure: " + ex.getClass().getSimpleName());
System.out.println("Message: " + message);
}
};
}
}
@Entity
@Table(name = "customers")
class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
private boolean active;
private LocalDateTime createdAt;
@Embedded
private Address address;
protected Customer() {
}
Customer(String firstName, String lastName, String email, boolean active, LocalDateTime createdAt, Address address) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.active = active;
this.createdAt = createdAt;
this.address = address;
}
@Override
public String toString() {
return firstName + " " + lastName + " <" + email + "> " + address + " active=" + active;
}
}
@Embeddable
class Address {
private String city;
private String street;
protected Address() {
}
Address(String city, String street) {
this.city = city;
this.street = street;
}
@Override
public String toString() {
return city + ", " + street;
}
}
interface CustomerRepository extends JpaRepository<Customer, Long> {
List<Customer> findByLastNameIgnoreCase(String lastName);
List<Customer> findByAddressCity(String city);
List<Customer> findByLastNameAndActiveTrueOrderByCreatedAtDesc(String lastName);
List<Customer> findTop3ByActiveTrueOrderByCreatedAtDesc();
long countByActiveTrue();
boolean existsByEmail(String email);
Optional<Customer> findByEmail(String email);
}Follow-up & Tricky Questions:
- How does Spring decide between a derived query, a named query, and
@Query? By default, Spring Data first looks for an explicitly declared query, then a named query, and then it falls back to query derivation. That means an annotated query wins when you need exact control. - Can derived queries navigate relationships? Yes, simple path traversal works, such as
findByAddressCityfor an embedded object or association. Once the path becomes a real join-heavy report, readability usually drops and@Querybecomes cleaner. - What return types can I use? Common ones are
List,Optional, a single entity,Page,Slice,long, andboolean. The return type should match the meaning of the query, not just whatever compiles. - How does sorting work in a derived query? You can hard-code it with
OrderByinside the method name, or pass aSort/Pageableparameter. If you need user-driven sorting, prefer parameters instead of baking every order into the method name. - Do derived queries save time at runtime? Not much compared with handwritten JPQL. The main win is developer speed and readability; the actual database work is what dominates latency.
- Can I use derived queries for deletes? Yes, methods like
deleteByStatusare supported, but they are destructive, so you should be careful with transactions and business rules. - Does
findTopBy...mean the newest row? Not by itself. Without an explicitOrderBy, the database can return any matching row, so the result is not stable. - Is
existsByjustcountBy(...) > 0? Conceptually, yes, butexistsByis clearer and may let the provider produce a more efficient query. - What happens if a field name is misspelled? Spring usually fails early with a property resolution error when the repository is created, which is good because you find the bug before the app serves traffic.
Common Mistakes:
- Making method names too long. If the name starts reading like a sentence, move to
@Queryor Specifications. - Assuming one row when the data is not unique. If the column is not guaranteed unique, return
Listor add a database unique constraint. - Forgetting that order is not automatic. Add
OrderByor useSort/Pageablewhen the row order matters. - Expecting every kind of condition to fit in the method name. Complex joins, subqueries, and report-style filters are usually better as explicit queries.
Memory Hook: Think of the method name like a recipe card: Spring reads the words, turns them into a query, and serves the result. If the recipe card becomes a novel, it is time to stop using it.
Cheat Sheet:
findBy...returns rows that match a property filter.countBy...returns a count.existsBy...returns a yes/no answer.TopandFirstlimit the result size, usually to 1 if no number is written.And,Or,IgnoreCase, andOrderByare common building blocks.- Use derived queries for simple cases; use
@Queryor Specifications when logic gets complex.
Practice Tasks:
- Add
findByFirstNameStartingWithand print the result for names starting withA. - Replace one list query with a
Page<Customer>method and try paging the results. - Break the method name on purpose, restart the app, and read the startup error so you recognize the failure pattern in real projects.