Specifications
Specifications provide a way to build dynamic, type-safe, reusable queries using the JPA Criteria API. They are ideal for complex filtering scenarios where query conditions are determined at runtime.
Think of Specifications as predicates (conditions) that can be combined using logical operators like AND/OR.
What is a Specification?
A Specification is a predicate builder for an entity. It implements the Specification<T> interface:
publicinterfaceSpecification<T> {
PredicatetoPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
- T → Entity type
- toPredicate() → Builds query conditions dynamically
- Composable → Can be combined using
and(), or(), not()
Why Use Specifications?

- Dynamic Querying
- Allows building queries at runtime based on available parameters.
- Perfect for search filters, where users may provide any combination of criteria.
- Type-Safe Queries
- Uses Criteria API, so queries are checked at compile time.
- Reduces errors caused by string-based JPQL queries.
- Reusable Query Logic
- Each condition can be encapsulated in a Specification method.
- Promotes code reuse across multiple services or modules.
- Composable Conditions
- Combine multiple specifications with AND / OR operators.
- Enables building complex queries without writing many repository methods.
- Supports Pagination & Sorting
- Works seamlessly with Spring Data’s
Pageable and Sort.
- Makes large datasets manageable in UI or API responses.
- Better Maintainability
- Keeps query logic separate from business logic.
- Easy to read, modify, and extend without changing repository methods.
- Ideal for Search/Filter APIs
- Use in scenarios where users provide optional filters.
- Avoids creating dozens of repository methods for every combination.
Creating a Simple Specification
Example: Filter users by status:
publicclassUserSpecifications {
publicstatic Specification<User>hasStatus(String status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
publicstatic Specification<User>isActive() {
return (root, query, cb) -> cb.isTrue(root.get("active"));
}
}
- Returns a Specification object encapsulating a condition
- Can be reused across multiple queries
Combining Specifications
Specifications can be combined dynamically:
Specification<User> spec = UserSpecifications.hasStatus("ACTIVE").and(UserSpecifications.ageGreaterThan(25));
List<User> users = userRepository.findAll(spec);
Specification.where() → Starts the chain
and() / or() → Combine multiple conditions dynamically
- Enables dynamic search filters for dashboards or admin panels
Repository Integration
To use Specifications, extend JpaSpecificationExecutor<T> along with JpaRepository:
publicinterfaceUserRepositoryextendsJpaRepository<User, Long>, JpaSpecificationExecutor<User> {}
- Methods available:
findAll(Specification<T> spec)
findOne(Specification<T> spec)
count(Specification<T> spec)
- Supports pagination and sorting:
Page<User> page = userRepository.findAll(spec, PageRequest.of(0,10, Sort.by("name")));
Advanced Example: Dynamic Search
Specification<User> spec = Specification.where(null);
if(name !=null) {
spec = spec.and(UserSpecifications.hasName(name));
}
if(active !=null) {
spec = spec.and(active ? UserSpecifications.isActive() : UserSpecifications.isInactive());
}
List<User> users = userRepository.findAll(spec);
- Builds queries conditionally based on input parameters
- Avoids creating multiple repository methods for every filter combination
Fluent API Usage (Spring Data 3.5+)
Spring Data JPA provides fluent query methods for advanced execution:
Page<UserDTO> page = userRepository.findBy(spec,
q -> q.as(UserDTO.class)
.page(PageRequest.of(0,20, Sort.by("lastname")))
);
Optional<User> firstUser = userRepository.findBy(spec,
q -> q.sortBy(Sort.by("createdDate").descending())
.first()
);
- Intermediate methods:
sortBy(), limit(), as(), project()
- Terminal methods:
first(), one(), all(), page(), slice(), stream()
- Gives fine-grained control over query execution and projections
PredicateSpecification / UpdateSpecification / DeleteSpecification
- PredicateSpecification: A flexible, query-agnostic predicate interface
- UpdateSpecification / DeleteSpecification: Specialized specifications for update or delete operations
publicstatic UpdateSpecification<User>updateStatusById(String status, Long id) {
return UpdateSpecification.<User>update((root, update, cb) -> update.set("status", status)).where(UserSpecifications.hasId(id));
}
Best Practices
- Keep each Specification focused on a single condition
- Combine Specifications in the service layer for complex filters
- Avoid putting business logic inside Specifications
- Use DTO projections for read-only queries to improve performance
Real-World Use Cases
- Advanced search screens with multiple optional filters
- Filterable dashboards
- Reporting modules requiring aggregation or conditional queries
- Admin panels with flexible search parameters
- E-commerce product filtering with multiple dynamic options
Conclusion
- Specifications are dynamic, reusable, and type-safe query predicates
- Built on Criteria API
- Combine multiple filters using AND/OR
- Integrates with JpaSpecificationExecutor for repository support
- Ideal for search forms, dashboards, dynamic filters, and reporting modules