Home/Learn/Hibernate & JPA/Specifications & Predicates

Specifications & Predicates

Advanced
Spring Data JPA

JpaSpecificationExecutor 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.

Java — Specification factory methods
// 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.

Java — dynamic spec composition + paginated search
@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.

Java — Specification with JOINs
// 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 Aria
1

What is the Specification pattern in Spring Data JPA?

MediumAmazon
2

How do you make a filter optional in a Specification?

MediumInfosys
3

How do you join another entity inside a Specification?

HardNetflix
4

When would you use Specifications instead of @Query with JPQL?

MediumPivotal
5

Why might you need query.distinct(true) when using Specifications with joins?

HardUber

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.

Loading discussion…