Spring Data JPA gives us simple methods like findByName() or custom queries using @Query. But when you need to build dynamic, complex queries at runtime (especially with multiple filters), Criteria API is the most powerful and flexible tool.
This post will walk you through:
- What is the JPA Criteria API
- Why use it instead of JPQL or native SQL
- How to use it in Spring Boot
- Advanced use cases like joins, predicates, pagination
- Best practices

π§ What is Criteria API?
The JPA Criteria API allows you to construct queries programmatically in Java using a type-safe, object-oriented API.
Itβs like building SQL queries without writing actual SQL.
β‘ Why Use Criteria API?
| When to Use | Why |
|---|---|
| Dynamic Filters | Build queries with optional parameters |
| Complex Joins and Subqueries | Easier than writing JPQL manually |
| Type Safety | No syntax errors at runtime |
| Compile-Time Checking | Refactor-friendly |
π§± Basic Building Blocks
CriteriaBuilder: Factory for query partsCriteriaQuery: Represents a query objectRoot: Represents table/entityPredicate: WHERE conditions
π§ͺ Example Setup
Let’s use a User entity:
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
private String status;
@ManyToOne
private Department department;
// Getters and setters
}
@Entity
public class Department {
@Id @GeneratedValue
private Long id;
private String name;
}
π§ 1. Basic Criteria Query
public List findUsersByStatus(String status) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(User.class);
Root user = query.from(User.class);
Predicate condition = cb.equal(user.get("status"), status);
query.select(user).where(condition);
return entityManager.createQuery(query).getResultList();
π§© 2. Multiple Conditions (Dynamic Filters)
public List findUsers(String name, String email, String status) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(User.class);
Root user = query.from(User.class);
List predicates = new ArrayList<>();
if (name != null) {
predicates.add(cb.like(user.get("name"), "%" + name + "%"));
}
if (email != null) {
predicates.add(cb.equal(user.get("email"), email));
}
if (status != null) {
predicates.add(cb.equal(user.get("status"), status));
}
query.select(user).where(cb.and(predicates.toArray(new Predicate[0])));
return entityManager.createQuery(query).getResultList();
}
β This is useful for building dynamic search filters in real-world applications.
π 3. Using Joins (e.g., User β Department)
public List findUsersByDepartment(String deptName) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(User.class);
Root user = query.from(User.class);
Join department = user.join("department");
Predicate predicate = cb.equal(department.get("name"), deptName);
query.select(user).where(predicate);
return entityManager.createQuery(query).getResultList();
}
π 4. Sorting and Pagination
public List getUsersSortedAndPaged(int page, int size, String sortField) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(User.class);
Root user = query.from(User.class);
query.orderBy(cb.asc(user.get(sortField)));
TypedQuery typedQuery = entityManager.createQuery(query);
typedQuery.setFirstResult(page * size);
typedQuery.setMaxResults(size);
return typedQuery.getResultList();
}
π― 5. Using CriteriaQuery for Count
Useful for pagination metadata:
public long countUsersWithStatus(String status) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(Long.class);
Root user = query.from(User.class);
query.select(cb.count(user)).where(cb.equal(user.get("status"), status));
return entityManager.createQuery(query).getSingleResult();
}
π§ 6. CriteriaBuilder with IN Clause
public List findUsersInDepartments(List deptNames) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(User.class);
Root user = query.from(User.class);
Join department = user.join("department");
Predicate predicate = department.get("name").in(deptNames);
query.select(user).where(predicate);
return entityManager.createQuery(query).getResultList();
}
ποΈ 7. Reusable Utility for Dynamic Filtering
A reusable method to build Predicate list dynamically:
private List buildFilters(CriteriaBuilder cb, Root root, Map filters) {
List predicates = new ArrayList<>();
filters.forEach((field, value) -> {
if (value != null) {
predicates.add(cb.equal(root.get(field), value));
}
});
return predicates;
}
Then use it in your repository:
public List searchUsers(Map filters) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(User.class);
Root user = query.from(User.class);
List predicates = buildFilters(cb, user, filters);
query.select(user).where(cb.and(predicates.toArray(new Predicate[0])));
return entityManager.createQuery(query).getResultList();
}
β Best Practices
| Practice | Why |
|---|---|
β
Use Metamodel for type safety (via @StaticMetamodel) | Prevents string-based errors |
| β Encapsulate criteria logic into service or helper | Keeps repository clean |
| β Use Criteria for complex, dynamic queries | Better than string-based JPQL |
| β Avoid mixing Criteria and native queries | Keep query styles consistent |
| β Donβt use Criteria for simple queries | Named queries or method names are easier |
π§ͺ Summary
- Criteria API is powerful for type-safe, dynamic queries
- Useful for filtering, joins, pagination, and sorting
- Consider using Specifications if you use Spring Data JPA (alternative abstraction)
- Donβt overuse it for basic queries