Dynamic queries with Specifications and Criteria
Some queries are not known until runtime. A search endpoint might filter by city, or status, or both, or
neither — depending on what the user typed. Writing a derived method or @Query for every combination is
impossible (there are too many), and building query strings by concatenation is unsafe and ugly. Spring
Data's Specifications (built on JPA's Criteria API) let you compose queries programmatically and
safely at runtime. This lesson is dynamic queries, verified against a running Dakiya.
The problem: queries built from optional filters
A parcel search takes several optional filters:
GET /api/parcels?city=Pune → filter by city
GET /api/parcels?status=BOOKED → filter by status
GET /api/parcels?city=Pune&status=BOOKED → both
GET /api/parcels → no filter
You cannot pre-write a finder for every subset of filters — with three optional filters there are eight
combinations, with more it explodes. And building the JPQL string by hand ("...where " + (city != null ? "p.city = '" + city + "'" : "")...) is both unsafe (injection) and unreadable. You need to compose the
query from whichever filters are present.
Specifications: composable query fragments
A Specification<T> is a small object representing a query condition — a WHERE fragment — built with
JPA's Criteria API. Each one is a lambda receiving the query root, the query, and a criteria builder:
Specification<Parcel> inCity(String city) {
return (root, query, cb) -> cb.equal(root.get("destinationCity"), city);
}
Specification<Parcel> hasStatus(Parcel.Status status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
root.get("destinationCity") refers to the entity field; cb.equal(...) builds an = condition — safely,
as a bound parameter, never string-concatenated. To use them, the repository extends
JpaSpecificationExecutor:
public interface ParcelRepository extends JpaRepository<Parcel, Long>, JpaSpecificationExecutor<Parcel> {}
Verified against the running app: parcelRepository.findAll(inCity("Pune")) returned the 3 Pune parcels — a
dynamic query, built at runtime, executed safely.
Composing them at runtime
The power is that specifications combine with and, or, and not, so you build the query from
whichever filters are actually present:
public List<Parcel> search(String city, Parcel.Status status) {
Specification<Parcel> spec = Specification.where(null); // start with "no condition"
if (city != null) spec = spec.and(inCity(city)); // add only if present
if (status != null) spec = spec.and(hasStatus(status));
return repository.findAll(spec); // one query with just the applied filters
}
This composes exactly the conditions the request supplied: city only, status only, both, or (starting from
where(null)) none — no combinatorial explosion of finder methods, no string building. Each applied
specification adds a bound-parameter condition to the generated SQL. This is the clean, safe way to build
the "filter by any combination of optional criteria" endpoint that almost every real API needs.
The Criteria API underneath
Specifications are a thin, pleasant layer over JPA's Criteria API — a programmatic, type-safe way to
build queries as Java objects rather than strings. You can use the Criteria API directly for very complex
dynamic queries (joins, subqueries, aggregates built at runtime), but it is verbose; Specifications wrap the
common case (composable WHERE conditions on one entity) in something far more readable. The trade-off to
know:
- Derived methods /
@Query— for fixed queries known at compile time. Most queries. Reach for these first. - Specifications — for dynamic queries composed from runtime conditions (a search with optional filters). Reach for these when the set of conditions is not fixed.
- Criteria API directly — for dynamic queries too complex even for Specifications. Rare.
Do not use Specifications for a query you could write as a simple derived method or @Query — they are
more machinery than a fixed query needs. They earn their place specifically when the query varies at runtime.
Why not just build the query string?
It is worth being explicit about the alternative you are avoiding. Concatenating a query from strings —
"select p from Parcel p where 1=1" + (city != null ? " and p.destinationCity = '" + city + "'" : "") — is
the anti-pattern Specifications exist to replace. It is:
- Unsafe — interpolating
cityinto the string is a SQL/JPQL injection hole (Specifications bind parameters, so they are safe). - Unreadable and error-prone — the
1=1trick, the spacing, the quoting — fragile string-plumbing. - Untyped — a typo in a field name is a runtime failure, where
root.get("...")at least centralises it.
Specifications give you dynamic composition with parameter binding and type-safety — the safety of the ORM
even when the query is built on the fly. When you meet a "search by any combination of filters" requirement,
reach for a Specification, not a StringBuilder.
Check your work
The problem. Optional filters produce too many query combinations to pre-write, and string-building a query is unsafe and unreadable.
Specifications. A Specification<T> is a composable WHERE fragment built with the Criteria API
(root.get("field"), cb.equal(...)), used via JpaSpecificationExecutor. Verified: findAll(inCity("Pune"))
returned the Pune parcels — a safe, runtime-built query.
Composing. spec.and(...), .or(...), starting from Specification.where(null); add each filter only
if present, so the query has exactly the applied conditions — no combinatorial finders, no string building.
When to use. Fixed queries → derived/@Query; dynamic queries from runtime conditions →
Specifications; extremely complex dynamic queries → Criteria API directly. Do not use Specifications for a
fixed query.
Why not string-build. Concatenating a query is unsafe (injection), unreadable, and untyped; Specifications bind parameters and are type-aware — the ORM's safety, dynamically.
Practice
- Extend
JpaSpecificationExecutorand writeinCityandhasStatusspecifications; runfindAll(inCity("Pune"))and confirm it returns the right parcels (reproduce the verified result). - Write a
search(city, status)that composes specifications only for the non-null filters; test all four combinations (city, status, both, none) and confirm each generates the rightWHERE. - Inspect the SQL (show-sql) for a composed specification and confirm the values are bound parameters, not inlined.
- Try to build the same dynamic query by string concatenation and note the injection risk and the unreadability — then contrast with the specification.
- Take a fixed query you wrote as a Specification and rewrite it as a derived method; reason about why the derived method is better for the fixed case.
- Sketch a query too complex for Specifications and note that the Criteria API (or a native query) is the fallback.
Official documentation
- Spring Data JPA — Specifications — Composable dynamic queries.
- Jakarta Persistence — Criteria API — The type-safe query API underneath.
- Spring Data — JpaSpecificationExecutor — Enabling specifications on a repository.
Next: performance — fetch strategies, batching and reading the SQL.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship