skip to content

When would you reach for the JPA Criteria API instead of a string JPQL query, and when is Criteria the wrong tool? Be concrete about the costs of each.

level: seniorimportance: should knowfreq 45%

answer

  1. same translator → same SQL, no perf argument
  2. dynamic shape / composition → Criteria
  3. fixed shape / readability → JPQL
  4. named queries validated at bootstrap
  5. StringBuilder JPQL = smell; conditionless criteria = smell

basics

~20 s

Use Criteria when the query shape varies at runtime — optional filters, dynamic sorting, composable predicates — or when compile-time attribute safety matters. Use string JPQL for fixed-shape queries: far more readable, reviewable, pasteable. Same translator, same SQL, so it is a maintainability call, not performance.

solid answer

~60 s

Both compile to the same SQL through the same Hibernate translator, so this is about **maintainability, not speed**. Choose Criteria when: - the **shape** varies per call — optional filters, dynamic ORDER BY, conditional joins; - predicates must be **composed and reused** across queries (list plus its count query, shared tenant/soft-delete filters); - you want **compile-time safety** on attribute names and types via the static metamodel, in a codebase that refactors entities often. Choose string JPQL when the shape is fixed: it reads like the SQL it becomes, a reviewer can verify it at a glance, you can paste a variant into a database console, and named queries get parsed and validated at bootstrap rather than on first call. Criteria's real costs: verbosity (three to five times the lines), poor readability for complex joins/aggregates, error messages that point at builder calls, a tree rebuilt per request, and awkwardness with vendor-specific SQL. JPQL's costs: string manipulation for dynamic parts, no compile-time checking, refactoring by search-and-replace. Most codebases end up mixed — and one clearly complex report is often better as a native query than as either.

code

java · 15 lines
java
// JPQL: one line, reviewable at a glance
List<Order> a = em.createQuery(
        "select o from Order o where o.status = :s order by o.createdAt desc",
        Order.class)
    .setParameter("s", Status.NEW)
    .getResultList();

// Criteria: same SQL, six lines, no upside when nothing varies
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> o = cq.from(Order.class);
cq.select(o)
  .where(cb.equal(o.get(Order_.status), Status.NEW))
  .orderBy(cb.desc(o.get(Order_.createdAt)));
List<Order> b = em.createQuery(cq).getResultList();

go deeper

for a junior

Know the headline: Criteria for queries whose shape changes at runtime, JPQL strings for fixed ones, and that both end up as the same SQL.

for a middle

Add composability and metamodel safety on one side, readability, tooling and bootstrap-validated named queries on the other.

for a senior

Give a defensible codebase policy with concrete signals for choosing wrong, and note where native SQL beats both.

for a principal

Frame it as a consistency and onboarding decision across teams — one query style per shape class, review heuristics, and where the read-model boundary sits relative to reporting.

## Same destination, different road A JPQL string is parsed into an internal query tree; a criteria query *is* that tree, built by hand. Hibernate translates both with the same machinery, so the SQL, the bind parameters, the plan cache behaviour and the runtime cost are the same. Anyone who claims one is inherently faster is guessing. The differences are entirely in the developer-facing properties: readability, checkability, composability, and how the code fails. ## What Criteria is genuinely good at **Runtime-varying shape.** The canonical case is a search screen with optional filters: the number of conditions is not known until the request arrives. Criteria expresses this as ordinary Java control flow over a `List<Predicate>`. The string alternative — concatenating fragments — is where `where 1=1`, dangling `AND`s and duplicated joins live. **Composability.** Predicates are first-class values, so filter logic becomes a method: one builder feeds both the page query and the count query; a shared method appends a tenant-id or soft-delete condition to every query in a module. Strings cannot be composed safely without a mini query builder of your own. **Compile-time safety.** With the static metamodel, an attribute rename breaks the build. String JPQL fails at runtime — mitigated for named queries, which Hibernate validates during bootstrap, but not for ad-hoc strings. **Programmatic generation.** Layers that build queries from metadata — generic admin filters, exported report definitions — have no sensible string alternative. ## What string JPQL is genuinely good at **Readability.** `select o from Order o join fetch o.lines where o.status = :s order by o.createdAt desc` is a sentence; the criteria equivalent is a paragraph of builder calls. In review, the string can be checked for correctness in seconds. **Fidelity to SQL thinking.** Aggregates with `group by`/`having`, multi-way joins, subqueries and set operations read close to the SQL you are picturing. In Criteria the same query is `Subquery` objects, `correlate` calls and casts — technically fine, cognitively expensive. **Tooling.** You can copy a JPQL query, adapt it, run it in a console. IDEs offer JPQL inspection and completion inside strings. Log output correlates obviously with the source. **Bootstrap validation.** Named queries (`@NamedQuery`) are parsed when the persistence unit starts, so a typo fails the application at startup rather than in production on a rare code path. ## The costs of Criteria, stated honestly - **Verbosity.** Expect three to five times the lines, plus imports and a builder variable threaded through. - **Readability cliff.** Simple filters read fine; the moment you need a correlated subquery with an aggregate, few reviewers can verify the code by reading it. - **Diagnostics.** A mistake surfaces as `IllegalArgumentException` or a provider error naming an internal node, not a line and column in a query. - **Per-request construction.** Allocation plus translation on each call. Real but tiny next to a round trip; explicit `ParameterExpression`s plus a cached `CriteriaQuery` are the fix if a hot path ever justifies it. - **Vendor features.** Window functions, hints, `INSERT ... SELECT`, exotic operators — some reachable via `cb.function`, some only via native SQL. ## A practical policy A policy that holds up in real codebases: **fixed-shape reads in JPQL (named queries when they are reused), genuinely dynamic reads in Criteria, and heavy reporting in native SQL projected into DTOs.** Do not let a criteria layer grow to cover queries whose shape never changes — that is pure cost. Conversely, do not build a homegrown JPQL string concatenator; if you find yourself writing one, you have re-implemented Criteria badly, without its safety. A middle path worth knowing: keep the *static* part of the query in JPQL and vary only the parts that must vary — for example a fixed JPQL query per sort column is often clearer than one dynamic criteria monster, when there are three sorts and no other variability. ## Signals you chose wrong A criteria method with no conditionals in it wants to be JPQL. A JPQL string built with `StringBuilder` and `if` statements wants to be Criteria. A criteria query with four nested subqueries and casts everywhere wants to be a native query with a DTO projection. Reviewers can apply those three heuristics without knowing the domain.

  • Is there any performance difference between a criteria query and the equivalent JPQL string?
    Not in the SQL or its execution — both go through the same Hibernate translator and produce the same statement with the same binds. The only measurable difference is on the Java side: building a criteria tree allocates objects each request, and JPQL parsing has its own cost that Hibernate caches by query string. Both are negligible against the database round trip, so the choice should be made on maintainability.
  • How would you keep a soft-delete or tenant-id condition on every read without repeating it in each query?
    With Criteria you factor it into a method that takes the builder and a root and returns the predicate, then include it in every predicate list — plain Java reuse. Hibernate also offers @Filter with session-level enablement, which applies to entity loads and HQL uniformly. The point in this comparison is that predicate composition is where Criteria pays for itself, whereas the string form forces you either to repeat the fragment or to build a concatenator.

saying these in an interview costs you the question

  • Claiming the Criteria API is faster (or slower) than equivalent JPQL
  • Mandating Criteria for every query in the name of type safety, including fixed-shape ones
  • Building JPQL with StringBuilder and if statements rather than using Criteria
  • Believing Criteria protects against SQL injection in a way parameterised JPQL does not
  • Treating native SQL as forbidden when a reporting query is genuinely awkward in both

context