Query Objects and Specifications
Purpose
Give queries a first-class representation when composition, reuse or dynamic filtering
justifies it — and keep them as plain statements when they do not. The Query Object pattern
can support safe reusable composition. Concatenating trusted fixed SQL fragments with bound
values is valid; interpolating untrusted values or identifiers is not. A
business criterion ("orders overdue for a premium customer") also deserves a name.
Two failures bracket the topic. The method explosion: a repository with 40 derived
finders, each a slight variation, none composable. The specification maze: a composable
DSL so indirect that nobody can predict the SQL, the fetch behaviour or the index usage from
reading the call site.
The options
Derived query method findByStatusAndCustomerId(...). Zero code,
self-documenting, not composable. Excellent
for a small fixed set of queries.
Named query / explicit JPQL or SQL, written once, named. Predictable,
statement reviewable, optimisable. Not composable.
Query Object an object holding criteria, translated to a
query by something that knows the storage.
Composable and testable.
Specification a predicate object over the domain, combinable
with and/or/not; the ORM's criteria API is the
usual implementation.
Type-safe query DSL a generated fluent API over the schema or the
entities. Composable and compile-checked.
Workflow
- Count the real variability. A screen with three optional filters has eight
combinations, not infinite ones — and eight is often better served by two or three named
queries than by a composable framework.
- Name the business criteria.
OverdueInvoices, ActiveSubscriptionsRenewingBefore.
If a criterion has a name in the business, it should have one in the code, whatever
mechanism implements it.
- Choose the mechanism per query, not per project. A repository can hold derived
methods, a named JPQL query and one specification-based search without inconsistency.
- Decide the result shape first. Most queries behind a screen want a projection, not
an entity — that decision usually matters more than the composition mechanism
(
architecture-and-performance).
- Read the generated SQL for anything composed. Composition hides joins, and a
specification that adds a join per predicate can change row multiplicity or existential
meaning. Join reuse must preserve type, ON clauses and same-child versus different-child intent.
- Test the composition, not just the parts. Individually correct predicates can combine
into wrong joins, NULL behavior or counts. Test content, count, existence and access scope.
Decision rules
A handful of fixed queries, each used in one place
→ derived methods or a named query. Adding a composition
framework here is pure overhead.
One search screen with optional filters
→ a query object holding the filter values, translated in one
place. Readable, testable, and the SQL is predictable.
The same business criterion is used in several queries and must stay
consistent (what counts as "active", "overdue", "billable")
→ a named specification. This is the strongest justification for
the pattern: one definition, many uses.
Filters must combine arbitrarily across many fields (an admin search,
a rules engine, a saved-search feature)
→ specifications or a type-safe DSL. Accept the indirection;
this is the case that earns it.
A report, an aggregation, a window function, a recursive query
→ compare explicit SQL with a capable DSL/provider API. Choose the
clearest supported expression and verify the generated plan.
A count or an existence check
→ a dedicated query. Loading entities to count them is the most
common needless cost in this area.
The query returns entities that are only read
→ consider a projection; bounded entity reads can also be appropriate.
Choose result shape from required data and behavior (repository-pattern).
Rules
- A query object is not a database abstraction. Its purpose is composition and naming,
not portability. Designing one so the storage could be swapped produces a lowest-common-
denominator API and usually still fails to be portable
(
architecture-decision-making).
- Derived query methods stop paying when names obscure intent, criteria repeat, optional parameters
cause combinatorial methods, or generated SQL becomes hard to predict. There is no meaningful
universal condition-count threshold; use reviewability and change frequency.
- Composition hides joins. Repeated to-many joins can multiply roots, while reused joins
can accidentally require predicates to match the same child. Choose join aliases or EXISTS
from the intended quantifiers; verify result rows, count and SQL. Reuse by attribute name
alone is insufficient.
- Name reusable business criteria explicitly. Generic field predicates can support a
constrained query builder but are not a substitute for domain names; validate their
fields, operators and complexity.
- Keep reusable predicates separable from pagination and sorting. A use-case query request
may contain both; cursors must bind their ordering and filters consistently.
- Allowlist sortable fields, directions and supported null semantics. Validated ORM property
paths are not inherently raw SQL injection, but interpolated identifiers and unsafe sort
expressions can be. Bind values and choose SQL fragments from trusted constants.
- Dynamic queries with wildly different shapes make the optimiser's job harder — parameter
sniffing and plan reuse can produce a plan good for one filter combination and terrible
for another. When one combination dominates, a dedicated statement for it is a legitimate
optimisation.
- Criteria, HQL and generated DSL support varies by version. Prefer explicit SQL when it
expresses complex operations more clearly; do not infer performance from syntax alone
(
data-source-patterns).
- Execute every materially distinct query shape in CI where feasible, prioritizing dynamic,
privileged and high-traffic paths. Combinatorial searches may require pairwise/property-based
coverage plus production telemetry rather than pretending every value combination was run
(
metadata-mapping).
- Read paths need not hydrate aggregates or use their write repository, but may still need
transactions for consistency or cursor lifecycle. Choose deliberately rather than routing
through the write model only for symmetry
(
architecture-and-performance).
Mandatory tenant/authorization predicates are not optional user filters. Obtain their scope
from trusted context and AND it outside any user-controlled OR/NOT expression; apply the same
scope to content, count, existence, export and subsequent fetch phases. An empty allowed scope
must deny results, not omit the restriction. Bound page size and query complexity.
Inspect the Java toolchain, Spring/Data/provider versions, generated metamodel and schema
before choosing APIs. Return the chosen representation, result/NULL/date/currency semantics,
mandatory scope, predicted SQL and a test covering the material composition risk. Missing
schema or version evidence leaves those choices conditional; no dependency upgrade is implied.
References
- Composition styles — derived methods, a plain query
object, JPA specifications and a type-safe DSL implemented over the same search screen,
with the composition traps (duplicated joins, wrong counts, lost fetches) and the naming
discipline that keeps specifications readable. Read when choosing a mechanism or
refactoring a repository that has outgrown derived methods.
- Query performance and result shape — projections
versus entities, counting and existence, pagination including keyset pagination, what
composition does to plans and indexes, streaming large results, and the query-budget test.
Read when a query is slow, returns too much, or is about to be written against a large
table.
1---2name: query-objects-and-specifications3description: Expressing queries as objects that can be composed, named and tested — Query Object, Specification, criteria builders, derived repository methods and explicit SQL — and choosing between them per query rather than adopting one style everywhere. Use when repository interfaces have grown dozens of findByAAndBAndCOrderByD methods, when a search screen with optional filters is being built by concatenating strings, when a Specification chain has become unreadable or produces a query nobody can predict, when dynamic filtering is needed across several entities, when a criteria query is being written for something a single SQL statement would express, when reads are being forced through the aggregate, or when a query object is being proposed as an abstraction over the database. Does not cover the collection abstraction over domain objects (repository-pattern), fetch strategies and N+1 (orm-behavioral-patterns), where mapping metadata lives (metadata-mapping), or index design and pagination at the database level.4---56# Query Objects and Specifications78## Purpose910Give queries a first-class representation when composition, reuse or dynamic filtering11justifies it — and keep them as plain statements when they do not. The Query Object pattern12can support safe reusable composition. Concatenating trusted fixed SQL fragments with bound13values is valid; interpolating untrusted values or identifiers is not. A14business criterion ("orders overdue for a premium customer") also deserves a name.1516Two failures bracket the topic. The **method explosion**: a repository with 40 derived17finders, each a slight variation, none composable. The **specification maze**: a composable18DSL so indirect that nobody can predict the SQL, the fetch behaviour or the index usage from19reading the call site.2021## The options2223```text24Derived query method findByStatusAndCustomerId(...). Zero code,25 self-documenting, not composable. Excellent26 for a small fixed set of queries.2728Named query / explicit JPQL or SQL, written once, named. Predictable,29statement reviewable, optimisable. Not composable.3031Query Object an object holding criteria, translated to a32 query by something that knows the storage.33 Composable and testable.3435Specification a predicate object over the domain, combinable36 with and/or/not; the ORM's criteria API is the37 usual implementation.3839Type-safe query DSL a generated fluent API over the schema or the40 entities. Composable and compile-checked.41```4243## Workflow44451. **Count the real variability.** A screen with three optional filters has eight46 combinations, not infinite ones — and eight is often better served by two or three named47 queries than by a composable framework.482. **Name the business criteria.** `OverdueInvoices`, `ActiveSubscriptionsRenewingBefore`.49 If a criterion has a name in the business, it should have one in the code, whatever50 mechanism implements it.513. **Choose the mechanism per query**, not per project. A repository can hold derived52 methods, a named JPQL query and one specification-based search without inconsistency.534. **Decide the result shape first.** Most queries behind a screen want a projection, not54 an entity — that decision usually matters more than the composition mechanism55 (`architecture-and-performance`).565. **Read the generated SQL** for anything composed. Composition hides joins, and a57 specification that adds a join per predicate can change row multiplicity or existential58 meaning. Join reuse must preserve type, ON clauses and same-child versus different-child intent.596. **Test the composition, not just the parts.** Individually correct predicates can combine60 into wrong joins, NULL behavior or counts. Test content, count, existence and access scope.6162## Decision rules6364```text65A handful of fixed queries, each used in one place66 → derived methods or a named query. Adding a composition67 framework here is pure overhead.6869One search screen with optional filters70 → a query object holding the filter values, translated in one71 place. Readable, testable, and the SQL is predictable.7273The same business criterion is used in several queries and must stay74consistent (what counts as "active", "overdue", "billable")75 → a named specification. This is the strongest justification for76 the pattern: one definition, many uses.7778Filters must combine arbitrarily across many fields (an admin search,79a rules engine, a saved-search feature)80 → specifications or a type-safe DSL. Accept the indirection;81 this is the case that earns it.8283A report, an aggregation, a window function, a recursive query84 → compare explicit SQL with a capable DSL/provider API. Choose the85 clearest supported expression and verify the generated plan.8687A count or an existence check88 → a dedicated query. Loading entities to count them is the most89 common needless cost in this area.9091The query returns entities that are only read92 → consider a projection; bounded entity reads can also be appropriate.93 Choose result shape from required data and behavior (repository-pattern).94```9596## Rules9798- **A query object is not a database abstraction.** Its purpose is composition and naming,99 not portability. Designing one so the storage could be swapped produces a lowest-common-100 denominator API and usually still fails to be portable101 (`architecture-decision-making`).102- Derived query methods stop paying when names obscure intent, criteria repeat, optional parameters103 cause combinatorial methods, or generated SQL becomes hard to predict. There is no meaningful104 universal condition-count threshold; use reviewability and change frequency.105- **Composition hides joins.** Repeated to-many joins can multiply roots, while reused joins106 can accidentally require predicates to match the same child. Choose join aliases or EXISTS107 from the intended quantifiers; verify result rows, count and SQL. Reuse by attribute name108 alone is insufficient.109- Name reusable business criteria explicitly. Generic field predicates can support a110 constrained query builder but are not a substitute for domain names; validate their111 fields, operators and complexity.112- Keep reusable predicates separable from pagination and sorting. A use-case query request113 may contain both; cursors must bind their ordering and filters consistently.114- Allowlist sortable fields, directions and supported null semantics. Validated ORM property115 paths are not inherently raw SQL injection, but interpolated identifiers and unsafe sort116 expressions can be. Bind values and choose SQL fragments from trusted constants.117- Dynamic queries with wildly different shapes make the optimiser's job harder — parameter118 sniffing and plan reuse can produce a plan good for one filter combination and terrible119 for another. When one combination dominates, a dedicated statement for it is a legitimate120 optimisation.121- Criteria, HQL and generated DSL support varies by version. Prefer explicit SQL when it122 expresses complex operations more clearly; do not infer performance from syntax alone123 (`data-source-patterns`).124- Execute every materially distinct query shape in CI where feasible, prioritizing dynamic,125 privileged and high-traffic paths. Combinatorial searches may require pairwise/property-based126 coverage plus production telemetry rather than pretending every value combination was run127 (`metadata-mapping`).128- Read paths need not hydrate aggregates or use their write repository, but may still need129 transactions for consistency or cursor lifecycle. Choose deliberately rather than routing130 through the write model only for symmetry131 (`architecture-and-performance`).132133Mandatory tenant/authorization predicates are not optional user filters. Obtain their scope134from trusted context and AND it outside any user-controlled OR/NOT expression; apply the same135scope to content, count, existence, export and subsequent fetch phases. An empty allowed scope136must deny results, not omit the restriction. Bound page size and query complexity.137138Inspect the Java toolchain, Spring/Data/provider versions, generated metamodel and schema139before choosing APIs. Return the chosen representation, result/NULL/date/currency semantics,140mandatory scope, predicted SQL and a test covering the material composition risk. Missing141schema or version evidence leaves those choices conditional; no dependency upgrade is implied.142143## References144145- [Composition styles](references/composition-styles.md) — derived methods, a plain query146 object, JPA specifications and a type-safe DSL implemented over the same search screen,147 with the composition traps (duplicated joins, wrong counts, lost fetches) and the naming148 discipline that keeps specifications readable. Read when choosing a mechanism or149 refactoring a repository that has outgrown derived methods.150- [Query performance and result shape](references/query-performance.md) — projections151 versus entities, counting and existence, pagination including keyset pagination, what152 composition does to plans and indexes, streaming large results, and the query-budget test.153 Read when a query is slow, returns too much, or is about to be written against a large154 table.