Specifications & Predicates
AdvancedJpaSpecificationExecutor lets you compose dynamic queries using Specification objects (reusable Criteria API predicates), ideal for complex filtered search endpoints.
Overview
Spring Data JPA Specifications wrap the JPA Criteria API in a functional interface — Specification<T> produces a Predicate from Root<T>, CriteriaQuery, and CriteriaBuilder. Specifications are composable with .and(), .or(), .not(). Extend JpaSpecificationExecutor<T> on the repository to gain findAll(Specification) and findAll(Specification, Pageable). This is the recommended approach for dynamic multi-criteria search endpoints where users can supply any combination of filters. Blaze Persistence or QueryDSL offer alternatives for very complex queries.
Defining Specifications
A Specification<T> is a functional interface that returns a JPA Predicate. Define reusable specs as static factory methods in a dedicated class.
// Repository — extend JpaSpecificationExecutor
public interface OrderRepository extends
JpaRepository<Order, Long>,
JpaSpecificationExecutor<Order> { // adds findAll(Specification, Pageable)
}
// Specification factory class
public class OrderSpecs {
public static Specification<Order> hasStatus(OrderStatus status) {
return (root, query, cb) ->
status == null ? cb.conjunction() // no filter if null
: cb.equal(root.get("status"), status);
}
public static Specification<Order> forCustomer(Long customerId) {
return (root, query, cb) ->
customerId == null ? cb.conjunction()
: cb.equal(root.get("customerId"), customerId);
}
public static Specification<Order> createdAfter(LocalDate from) {
return (root, query, cb) ->
from == null ? cb.conjunction()
: cb.greaterThanOrEqualTo(root.get("createdAt"),
from.atStartOfDay());
}
public static Specification<Order> totalBetween(BigDecimal min, BigDecimal max) {
return (root, query, cb) -> {
if (min == null && max == null) return cb.conjunction();
if (min == null) return cb.lessThanOrEqualTo(root.get("total"), max);
if (max == null) return cb.greaterThanOrEqualTo(root.get("total"), min);
return cb.between(root.get("total"), min, max);
};
}
}Composing Specifications
Compose specs with .and(), .or(), .not(). Build the final spec dynamically based on which filters the user supplied. Pass it to findAll() with a Pageable for paginated results.
@Service
public class OrderSearchService {
private final OrderRepository orderRepository;
public Page<Order> search(OrderSearchRequest req, Pageable pageable) {
// Build spec dynamically — only apply filters that were provided
Specification<Order> spec = Specification.where(null); // always-true base
spec = spec.and(OrderSpecs.hasStatus(req.getStatus()));
spec = spec.and(OrderSpecs.forCustomer(req.getCustomerId()));
spec = spec.and(OrderSpecs.createdAfter(req.getFrom()));
spec = spec.and(OrderSpecs.totalBetween(req.getMinTotal(), req.getMaxTotal()));
return orderRepository.findAll(spec, pageable);
}
}
// REST endpoint
@GetMapping("/api/orders/search")
public Page<OrderDTO> searchOrders(
@RequestParam(required = false) OrderStatus status,
@RequestParam(required = false) Long customerId,
@RequestParam(required = false) @DateTimeFormat(iso=DATE) LocalDate from,
@RequestParam(required = false) BigDecimal minTotal,
@RequestParam(required = false) BigDecimal maxTotal,
Pageable pageable) {
OrderSearchRequest req = new OrderSearchRequest(
status, customerId, from, null, minTotal, maxTotal);
return orderSearchService.search(req, pageable)
.map(orderMapper::toDTO);
}Joins in Specifications
Use root.join() in the Specification to add JOINs for filtering on related entity fields. Call query.distinct(true) when joining a collection to prevent duplicate rows.
// Filter orders by customer name — join to customer entity
public static Specification<Order> customerNameContains(String name) {
return (root, query, cb) -> {
if (name == null || name.isBlank()) return cb.conjunction();
// Join to customer entity
Join<Order, Customer> customer = root.join("customer", JoinType.INNER);
query.distinct(true); // prevent duplicates from join
return cb.like(cb.lower(customer.get("name")),
"%" + name.toLowerCase() + "%");
};
}
// Filter orders that contain a specific product SKU
public static Specification<Order> containsSku(String sku) {
return (root, query, cb) -> {
if (sku == null) return cb.conjunction();
Join<Order, OrderItem> items = root.join("items", JoinType.INNER);
Join<OrderItem, Product> product = items.join("product", JoinType.INNER);
query.distinct(true);
return cb.equal(product.get("sku"), sku);
};
}
// Usage — combine join specs with attribute specs
Specification<Order> spec = OrderSpecs.customerNameContains("Alice")
.and(OrderSpecs.hasStatus(OrderStatus.PLACED))
.and(OrderSpecs.containsSku("WIDGET-001"));Key Points to Remember
- 1Specification<T> wraps JPA Criteria API — returns a Predicate from Root, CriteriaQuery, CriteriaBuilder.
- 2Extend JpaSpecificationExecutor<T> on the repository to get findAll(Specification, Pageable).
- 3Compose with .and(), .or(), .not() — start with Specification.where(null) as the base.
- 4Return cb.conjunction() (always true) when the filter argument is null — safe skip.
- 5Add root.join() inside a Specification to filter on related entity fields.
- 6Call query.distinct(true) when joining a collection to prevent duplicate result rows.
Interview Questions
Sign in to ask AriaWhat is the Specification pattern in Spring Data JPA?
How do you make a filter optional in a Specification?
How do you join another entity inside a Specification?
When would you use Specifications instead of @Query with JPQL?
Why might you need query.distinct(true) when using Specifications with joins?
Ask Aria about Specifications & Predicates
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.