CodeGym /Courses /Spring Data JPA /Dynamic search: Specification

Dynamic search: Specification

Spring Data JPA
Level 15, Lesson 1
Available

1. Specification<T>: the WHERE filter

The word specification sounds like GOST is about to show up and demand a stamp on every predicate. In reality, in Spring Data JPA this is a very practical thing: Specification<T> is an object that describes one condition (or a set of conditions) that ends up in the SQL WHERE clause for entity T. Not “execute a query,” not “read data,” but specifically describe a condition that the repository can later apply.

The main idea that beginners need to catch: Specification<Product> is not “yet another way to write a query,” but a way to assemble a query from small parts. You can make a separate specification for “product is active,” another for “in category N,” another for “price from X,” and then combine them with .and(...) or .or(...).

At the same time, a specification is always tied to one entity through a generic parameter. That is, Specification<Product> applies to Product, Specification<StockItem> to StockItem, and so on. This helps discipline the code: we are not building a “universal search engine for the whole project,” but solving a specific read use case for a specific model.

Under the hood, Specification uses the JPA Criteria API (there will be Root, CriteriaBuilder, Predicate), but today we are not going into the mechanics. The important thing is to understand the shape: a specification is not a JPQL string and not SQL, but a “condition as an object.” And that means it can be passed around, combined, tested, and reused.

To make it concrete: if you used to write something like this:

select *
from product p
where p.status = 'ACTIVE'  -- filter by status
  and p.price >= 10        -- and add a price threshold

then in the world of Specification this would be roughly “hasStatus(ACTIVE) AND priceGte(10).” Not as text, but as objects.

Another practical nuance: we almost always need a neutral starting point so that conditions can be added conveniently only when a filter field is filled in. In modern Spring Data JPA there is a neutral specification for this. In our course baseline it is convenient to use:

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

// Neutral start: a specification that "does not filter anything".
// Useful when you will later conditionally add .and(...) only if filter fields are filled in.
Specification<Product> spec = Specification.unrestricted();

If you ever see old examples from the internet, you will often come across Specification.where(null) — in meaning, it is the same “empty filter,” just in a different style. We stick to the modern API in the course, but it is useful to recognize “relatives” from the legacy world so you do not get startled by someone else’s code.

2. JpaSpecificationExecutor<T>: running specifications

Imagine that Specification<T> is a neatly assembled plug, and JpaSpecificationExecutor<T> is a socket. The plug is beautiful on its own, but until you have somewhere to plug it in, there will be no light. It is the same here: a specification describes a condition, but does not execute the query by itself. The repository executes the query, and for that it needs the JpaSpecificationExecutor<T> extension.

Technically, it is very simple: your repository remains a JpaRepository (that is, CRUD and everything familiar are still there), but in addition it starts extending JpaSpecificationExecutor. As a result, Spring Data creates a repository bean with extra methods such as findAll(spec), findAll(spec, pageable), count(spec), and so on.

Here is a minimal example for our shop-data-jpa project. We extend the product repository because products are exactly where an “admin search with a bunch of filters” usually appears.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

// JpaRepository gives CRUD, JpaSpecificationExecutor — methods findAll(spec), count(spec), exists(spec), etc.
public interface ProductRepository
        extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {
    // We do not write the implementation: Spring Data will generate everything itself.
}

Pay attention to an important thing: we do not write the implementation. Just like with ordinary repositories, Spring Data creates everything for us. We only say: “Here is my entity, here are my basic CRUD methods, and I also want to be able to execute specifications.”

It is useful to imagine the execution pipeline as a small diagram. Not for a framework exam, but for common sense — to understand where SQL is born in this process.

flowchart TD
    %% The service builds a filter (Specification) and passes it to the repository.
    A["Service / Query-service"] --> B["Specification<Product>"]
    B --> C["ProductRepository.findAll(spec, pageable)"]
    C --> D["Spring Data JPA + Criteria"]
    D --> E[Hibernate]
    E --> F[("PostgreSQL")]
    F --> E
    E --> C
    C --> A

We are not going deep into Criteria today, but it is important to remember: JpaSpecificationExecutor is not a separate framework, but just a standard bridge from “specification” to “executable query.”

And one more disciplining point. You do not need to add JpaSpecificationExecutor “just in case” to every repository. As soon as you do, the project gets a temptation: “let’s write all queries as specifications.” It is a bit like buying a powerful cordless drill and starting to stir soup with it. Great tool, but not for every situation.

3. JpaSpecificationExecutor methods

When we say “we connected JpaSpecificationExecutor,” it is useful to immediately see what exactly you get. It is not a magical single search() method, but a set of standard operations that cover most admin read scenarios: get a list, get a page, get one result, count, and check existence. And honestly, that is already a lot — the main thing is not to try to learn everything in one day like spells.

Below is a table with the “practical meaning” of the main methods. I explain them not only by type, but also by what they mean at the level of thinking.

Repository method (idea) What it returns When it is appropriate What the typical trap is
findAll(spec) List<T> You need a small list without pagination It is easy to accidentally pull “everything from the database”
findAll(spec, sort) List<T> You need a small list + ordering Sorting is a separate axis; do not try to “stuff ORDER BY into the specification”
findAll(spec, pageable) Page<T> You need a result page For Page there will almost always be a count query (and that is normal, but it is the price)
findOne(spec) Optional<T> You expect at most one record If there are multiple records, you will get a result size exception
count(spec) long You need the number of matching records Do not confuse it with findAll().size() — that is different in cost
exists(spec) boolean You need the fact that “there is at least one” This is not “always fast,” but often cheaper than loading the entity

The examples below are intentionally low-level: we take an already prepared Specification and look specifically at what the executor API can do. In a real admin search, it is more convenient to keep the external contract through ProductAdminFilter, and build the Specification inside the query service.

Let us make a few very short examples to see what this looks like in service code. In these examples we will use the stub ProductSpecifications.active() — we will discuss its internals in the next lecture, and for now we will assume that such a method already exists and returns Specification<Product>.

Example: get a list of active products and sort by price

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

public List<Product> findActiveCheapestFirst() {
    // 1) Build the filter: "which records match"
    Specification<Product> spec = ProductSpecifications.active();

    // 2) Build sorting separately: "in what order to return"
    Sort sort = Sort.by("price").ascending();

    // 3) The repository executes the query, applying WHERE (spec) and ORDER BY (sort)
    return productRepository.findAll(spec, sort);
}

The key idea here is that the specification is responsible for “which records match,” and sorting is responsible for “in what order.” These are different questions, and that is exactly how it should be.

Example: get a page of results for the admin panel

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

public Page<Product> findActivePage(Pageable pageable) {
    // The filter remains "clean" (only WHERE)
    Specification<Product> spec = ProductSpecifications.active();

    // Pageable controls page size, page number, and (optionally) sorting
    return productRepository.findAll(spec, pageable);
}

This option immediately reminds us of an old topic: Page is not just a list, it is “list + total count.” Therefore, under the hood there will most often be two queries: one for the page data, the second for count. That is normal and honest. The important thing is to expect this cost and choose Page consciously.

Example: find exactly one product by specification (careful)

import java.util.Optional;
import org.springframework.data.jpa.domain.Specification;

public Optional<Product> findSingleActiveWithSku(String sku) {
    // Combine specifications: active AND hasSku(sku)
    Specification<Product> spec = ProductSpecifications.active()
            .and(ProductSpecifications.hasSku(sku));

    // findOne = "zero or one". If there is more than one, an exception will be thrown.
    return productRepository.findOne(spec);
}

The findOne(spec) method is convenient, but it requires discipline: it assumes the result is either one or zero. If the specification is broad (for example, just active()), then it is almost guaranteed not to be one product, and Spring Data will complain.

Example: count how many products are “in stock”

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

public long countInStock() {
    // We do not need entities here: ask the database to return a number
    Specification<Product> spec = ProductSpecifications.inStock();

    // In SQL this becomes COUNT(...) with the same WHERE conditions
    return productRepository.count(spec);
}

The idea is simple: instead of pulling a list and counting it in Java, we honestly ask the database to count.

Example: check whether there is at least one “problematic” product

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

public boolean hasTooExpensiveProducts() {
    // NB: BigDecimal is usually imported from java.math.BigDecimal (the import is omitted here for brevity)
    Specification<Product> spec = ProductSpecifications.priceGte(new BigDecimal("10000"));

    // exists = "is there at least one record", without loading the entity itself
    return productRepository.exists(spec);
}

exists(spec) is useful when you need the fact itself. For example, you want to show a badge in the admin panel saying “there are expensive products” or “there are products with zero stock.” Do not load entities just to answer “yes/no.”

And last, but very important: a specification by itself does not know how to “turn on pagination” and does not replace Pageable. It only participates in forming the WHERE clause. Everything related to the shape of the result (List, Page, sorting) remains a decision at the repository call level.

4. Integrating into shop-data-jpa

When you first connect JpaSpecificationExecutor, a typical question arises: “Where should all this go so the project does not turn into chaos?” That is an excellent question, because specifications are easy to turn either into a “pile of static methods in the repository” or into a “shared utility for the whole project” that then depends on everything at once. Our task is to stay within the package-by-feature architecture and not break it.

Within shop-data-jpa, it makes sense to do this: the repository gets the ability to execute specifications, and the specifications themselves (as a set of search rules) live next to the read-side logic, that is, in catalog.query. Then the repository remains an “infrastructure” data-access layer, and the query logic remains the “application” part of reading.

The minimal set of steps looks very calm.

First, extend the repository:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

// The repository is still CRUD-based, but now it can accept Specification in findAll/count/exists.
public interface ProductRepository
        extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {
}

Then create a class for specifications (for now, even without implementation details). The main thing is the place and the shape:

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

public final class ProductSpecifications {

    private ProductSpecifications() {
        // Prevent instantiation: this is a utility class with static methods.
    }

    public static Specification<Product> active() {
        // root  — the Product "table" in Criteria terms
        // cb    — predicate factory (equal, like, and/or, etc.)
        // Return a Predicate that will go into WHERE: status = ACTIVE
        return (root, query, cb) -> cb.equal(root.get("status"), ProductStatus.ACTIVE);
    }
}

Yes, inside we already see root/query/cb, but do not panic: today what matters is that the method returns Specification<Product>. In the next lecture, we will look at who Root is, what CriteriaBuilder does, and why toPredicate(...) is the heart of a specification.

Inside a query service, this usually looks like this. The service expresses the use case, and the repository remains the place of execution.

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

public Page<Product> findActive(Pageable pageable) {
    Specification<Product> spec = ProductSpecifications.active();
    return productRepository.findAll(spec, pageable);
}

Notice how readable this is: the service speaks in human terms (“find active”), the filter is assembled inside (Specification), and the repository executes it. This is an intentionally low-level piece: here we are simply looking at the mechanics of findAll(spec, pageable) without extra noise. Externally, it is still more convenient to expose admin search as ProductAdminFilter + Pageable, and build the Specification inside.

For now, that is enough: the repository can handle specifications, and the project has a clear place where they live. Now it is important to learn how to write the small conditions themselves, from which the final where will be assembled.

5. Specification vs Sort and Pageable

Specification is responsible only for WHERE. Sorting and pagination live separately, because they answer a different question: not which records match, but in what order and in what chunk to return them. That is why a call like productRepository.findAll(spec, pageable) reads correctly: spec selects rows, and pageable adds ORDER BY, page size, and offset.

That is enough for now. For dynamic search, it is more important for us to learn how to assemble the conditions from optional fields than to discuss result containers.

6. findBy(spec, q -> ...): overview

JpaSpecificationExecutor also has a more flexible findBy(spec, q -> ...). The only fact we need here is this: on top of the same WHERE, it lets you choose a different result shape — for example, a projection or Slice instead of Page.

Specification does not turn into a projection because of that and does not start controlling pagination. It still only handles filtering.

7. Common mistakes when working with specifications

Mistake #1: forget to add JpaSpecificationExecutor<T> and try to call findAll(spec, ...).
This is the most annoying mistake, because you already wrote the specifications, already assembled the filter, already feel like an architect... and then the IDE says: “the method does not exist.” Almost always the reason is that the repository extends only JpaRepository, and you did not add JpaSpecificationExecutor.

Mistake #2: add JpaSpecificationExecutor “just in case” to all repositories.
At first it seems like “standardization,” and then the silent horror begins: all queries start being written as specifications, even the simplest ones. In the end, instead of a single readable findBySku(String sku), you get “build a specification by sku, run findOne().” It works, but it is harder to maintain than it needs to be.

Mistake #3: mix business logic and data filtering.
A specification is about reading. It should not decide whether a user can place an order, should not mutate data, and should not turn into a “microservice inside a predicate.” If you catch yourself thinking, “let’s also check this business invariant in the specification,” it is usually better to move that into the service and keep the specification clean.

Mistake #4: use findOne(spec) where the result is not guaranteed to be unique.
findOne sounds like “well, just find one,” but its semantics are different: “find zero or one, but not more.” If the specification is broad, you will get an exception about the wrong result size. For “find at least one,” use exists(spec) or findAll(spec, PageRequest.of(0, 1)) (depending on the task).

Mistake #5: expect the specification to handle pagination and sorting on its own.
This is a common confusion after working with JPQL, where you can write order by directly in the query. In Specification, conditions and result shape are intentionally separated. You pass Sort and Pageable separately, and you should accept that as the normal style. It makes the code more flexible and the filter reusable.

1
Task
Spring Data JPA, level 15, lesson 1
Locked
First Repository with JpaSpecificationExecutor
First Repository with JpaSpecificationExecutor
1
Task
Spring Data JPA, level 15, lesson 1
Locked
Counting and exists via Specification
Counting and exists via Specification
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION