skip to content

When would you choose Specifications over QueryDSL, Query-by-Example, or @Query, and what are their trade-offs at scale?

level: principalimportance: should knowfreq 30%

answer

  1. Specs = dynamic + reusable + Criteria type-safe
  2. QueryDSL = fluent DSL, apt build step
  3. QBE = AND-only equality/like, no OR/range/join
  4. @Query = fixed/complex/perf, but static
  5. scale: plan-cache variance, count queries, injection on native

basics

~20 s

Use Specifications for dynamic, reusable, composable filters built at runtime with type-safe Criteria. Prefer @Query for fixed complex queries, Query-by-Example for simple probe matching, and QueryDSL when you want a fluent DSL and heavy compile-time safety.

solid answer

~50 s

Specifications shine for dynamic filtering: optional, runtime-composed predicates you reuse via static factories and combine with and()/or(). They're built on the JPA Criteria API — type-safe (with the metamodel), no string parsing — and integrate with Pageable/Sort/@EntityGraph. Trade-off: Criteria code is verbose and hard to read for complex queries. QueryDSL offers a more fluent, readable DSL with strong compile-time safety, but needs an annotation-processor build step and an extra dependency. Query-by-Example is trivial for 'match these non-null fields with equality/like', but can't express ranges, OR, or joins — too limited for real search. @Query (JPQL/native) is best for fixed, complex, or performance-tuned queries but is static — dynamic optionality means string-building or many methods. At scale I weigh readability, query-plan predictability, and metamodel/refactor safety; often Specifications for search endpoints, @Query for hot fixed paths, QueryDSL if the team already uses it.

code

java · 18 lines
java
// A repository can expose several mechanisms; pick per use case.
public interface UserRepository extends
        JpaRepository<User, Long>,
        JpaSpecificationExecutor<User>,        // dynamic Specifications
        QuerydslPredicateExecutor<User>,       // QueryDSL predicates
        QueryByExampleExecutor<User> {         // simple probe matching

    // Fixed, tuned query with an explicit count query for pagination.
    @Query(value = "select u from User u where u.status = :s and u.createdAt > :t",
           countQuery = "select count(u) from User u where u.status = :s and u.createdAt > :t")
    Page<User> activeSince(@Param("s") UserStatus s, @Param("t") Instant t, Pageable p);
}

// QBE: trivial, but only AND of equality/like — no OR, ranges, or joins.
User probe = new User();
probe.setStatus(UserStatus.ACTIVE);
ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreNullValues();
List<User> found = userRepository.findAll(Example.of(probe, matcher));

go deeper

for a junior

Knows Specifications exist for dynamic queries.

for a middle

Can contrast Specifications vs @Query and vs derived queries.

for a senior

Articulates QBE limits and when @Query beats Specs; considers count queries and readability.

for a principal

Reasons about plan-cache variance, refactor safety via metamodel, team tooling cost of QueryDSL, and sets a per-use-case selection policy.

**The four mechanisms.** 1. **Specifications** (`JpaSpecificationExecutor` + `Specification`): programmatic Criteria predicates, composed at runtime with `and`/`or`/`not`/`allOf`/`anyOf`. Reusable fragments via static factories. Integrates with `Pageable`, `Sort`, `@EntityGraph`, `count`, `exists`, `delete`. Best for **dynamic/optional filters and predicate reuse**. 2. **QueryDSL** (`QuerydslPredicateExecutor` + generated `Q` classes): fluent, type-safe DSL (`QUser.user.status.eq(ACTIVE).and(...)`). Strong compile-time safety and readability. Cost: extra dependency + annotation processor generating `Q` types on every build; another abstraction for the team to learn. 3. **Query-by-Example (QBE)** (`QueryByExampleExecutor`, `Example.of(probe, matcher)`): build a probe entity, Spring matches non-null fields. `ExampleMatcher` tunes string matching (contains/startsWith), case sensitivity, ignored paths. **Limits**: only conjunction (AND) of equality/like on scalar fields; **no OR, no ranges (>, <, between), no joins/associations, no null-value matching**. Great for simple admin filters, useless for rich search. 4. **@Query (JPQL or native SQL)**: hand-written query on the repo method. Best control, best for **fixed complex queries**, aggregate/reporting, DB-specific SQL, and performance tuning (hints, index-friendly shapes). **Static** — dynamic optionality forces either many method variants or fragile string concatenation (and native concatenation risks injection). **Decision guide.** - Optional filters, unknown-at-compile-time combinations, reuse across queries -> **Specifications**. - Same needs but team wants a nicer DSL / maximal type safety and accepts the build tooling -> **QueryDSL**. - Trivial equality/like filter form with a few fields, no OR/ranges -> **QBE**. - Fixed, complex, or perf-critical query, or native features -> **@Query**. **Scale considerations.** - **Readability/maintainability**: Criteria (Specifications) is the most verbose; complex nested logic becomes hard to follow — QueryDSL or @Query read better there. - **Type safety / refactoring**: Specifications need the JPA static metamodel to avoid stringly-typed `get("field")`; QueryDSL is type-safe by construction; @Query strings aren't checked (though `spring-data-jpa` can validate JPQL at startup). - **Query-plan predictability**: dynamically composed WHERE clauses produce many distinct SQL shapes -> more prepared-statement/plan-cache variants and less predictable DB caching than a handful of fixed `@Query`s. For very hot paths, fixed queries can be easier to tune and cache. - **Pagination/count**: Specifications and QueryDSL auto-derive count queries; complex joins/distinct can make counts slow — sometimes a dedicated `countQuery` on `@Query` is better. - **Injection**: Criteria/Specifications and QueryDSL build parameterized queries structurally (safe). Native `@Query` string-building is where injection risk lives — always bind parameters, never concatenate user input. - **Testing**: predicate factories are unit-testable in isolation (compose and assert), a real plus for Specifications/QueryDSL over sprawling derived-method lists. **Combining them.** They coexist on one repository: extend both `JpaSpecificationExecutor` and, say, add `@Query` methods. Query-by-Example and Specifications can't be mixed in one call, but you can pick per use case. **Anti-patterns.** (1) Forcing everything into Specifications, yielding unreadable Criteria for genuinely fixed queries. (2) Building native SQL by string concatenation to fake dynamic filters (injection + no caching). (3) Choosing QueryDSL for one screen and paying the build-tooling tax project-wide. (4) Using QBE then discovering you need OR/ranges and rewriting anyway.

  • Why can Query-by-Example not replace Specifications for a search endpoint?
    QBE only ANDs equality/like matches on non-null scalar fields. It can't express OR, ranges (>, <, between), negation, or association/join filters, which real search screens routinely need. Specifications (or QueryDSL) handle all of those.
  • What downside do dynamically composed queries have on the database side at scale?
    Every filter combination yields a different SQL text, producing many prepared-statement and query-plan-cache variants. That's less predictable and less cache-friendly than a small set of fixed @Query shapes, which matters for very hot paths.

saying these in an interview costs you the question

  • Claiming Query-by-Example supports OR/ranges/joins — it's AND-only equality/like on scalar fields.
  • Saying QueryDSL needs no build tooling — it requires an annotation processor generating Q classes.
  • Forcing all queries into Specifications even when fixed @Query is clearer and easier to tune.
  • Building dynamic native SQL via string concatenation (injection risk, no plan reuse).

context