java 147 lines · 4 tabs

Dynamic Product Filtering with JPA Specifications from Optional Query Params

Shared by codesnips Aug 2026
4 tabs
package com.shop.catalog;

import org.springframework.data.jpa.domain.Specification;

import java.math.BigDecimal;

public final class ProductSpecifications {

    private ProductSpecifications() {
    }

    public static Specification<Product> nameContains(String name) {
        if (name == null || name.isBlank()) {
            return null;
        }
        String pattern = "%" + name.toLowerCase() + "%";
        return (root, query, cb) ->
                cb.like(cb.lower(root.get("name")), pattern);
    }

    public static Specification<Product> inCategory(Long categoryId) {
        if (categoryId == null) {
            return null;
        }
        return (root, query, cb) ->
                cb.equal(root.get("category").get("id"), categoryId);
    }

    public static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) {
        if (min == null && max == null) {
            return null;
        }
        return (root, query, cb) -> {
            if (min != null && max != null) {
                return cb.between(root.get("price"), min, max);
            }
            if (min != null) {
                return cb.greaterThanOrEqualTo(root.get("price"), min);
            }
            return cb.lessThanOrEqualTo(root.get("price"), max);
        };
    }

    public static Specification<Product> inStock(Boolean inStock) {
        if (inStock == null || !inStock) {
            return null;
        }
        return (root, query, cb) ->
                cb.greaterThan(root.get("stockQuantity"), 0);
    }
}
4 files · java Explain with highlit

This snippet shows how a product-listing endpoint can accept a handful of optional query parameters and translate only the ones that are present into a composed JPA Specification. The core idea is that filtering logic should not be a growing tangle of if/else branches building HQL strings; instead each filter is an independent, reusable predicate that the framework combines dynamically at query time.

In ProductSpecifications, each static factory returns a Specification<Product> for a single concern — nameContains, inCategory, priceBetween, and inStock. Every method guards against a null input and returns null in that case, which is the signal Spring Data uses to skip a predicate when specifications are combined. nameContains lowercases both sides so the match is case-insensitive, and priceBetween handles the min-only, max-only, and both-bounds cases with criteriaBuilder.greaterThanOrEqualTo and lessThanOrEqualTo. Writing predicates this granular keeps them composable and independently testable.

ProductFilter is a plain carrier for the optional parameters. Because Spring MVC binds request parameters onto its fields, absent params simply stay null, which lines up perfectly with the null-skipping behavior in the specifications. Keeping this separate from the entity avoids leaking query concerns into the domain model.

ProductRepository extends both JpaRepository and JpaSpecificationExecutor<Product>; the latter is what unlocks the findAll(Specification, Pageable) overload used for paged, filtered queries. No custom query methods are needed.

ProductController is where composition happens. Using Specification.where(...) as a null-safe starting point, it chains .and(...) across every filter. Because and treats a null spec as a no-op, only the parameters actually supplied contribute to the generated SQL — a request with no params yields an unrestricted, paginated fetch. The Pageable argument is resolved automatically from page, size, and sort params, so pagination and sorting come for free.

The main trade-off is that Criteria-based specifications are more verbose than a hand-written query, but they scale far better as filter combinations grow and eliminate the SQL-injection risk of string concatenation. A common pitfall is forgetting the null guard inside a specification, which would force an always-true or broken predicate; returning null is the idiomatic way to opt out. This pattern is the natural choice whenever an endpoint has many optional, orthogonal filters.


Related snips

ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
graphql
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    createdAt: String!

GraphQL API with Spring Boot

java graphql spring-boot
by David Kumar 3 tabs
java
package com.example.starter.config;

import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;

Custom Spring Boot starters

java spring-boot starter
by David Kumar 4 tabs
java
package com.example.demo.config;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;

Messaging with Apache Kafka

java kafka messaging
by David Kumar 3 tabs
ruby
module Paginatable
  extend ActiveSupport::Concern

  MAX_PER_PAGE = 100
  DEFAULT_PER_PAGE = 25

API Pagination Headers (Link + Total)

rails pagination rest-api
by codesnips 3 tabs
java
package com.example.demo.controller;

import com.example.demo.dto.FileMetadata;
import com.example.demo.service.FileStorageService;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;

File upload and download handling

java spring-boot file-upload
by David Kumar 2 tabs

Share this code

Here's the card — post it anywhere.

Dynamic Product Filtering with JPA Specifications from Optional Query Params — share card
Link copied