Interview question
Build a JPA projection query with optional filters
Builds a bounded dynamic query that returns a read model, applies only supplied predicates, and uses deterministic ordering.
TL;DR
Builds a bounded dynamic query that returns a read model, applies only supplied predicates, and uses deterministic ordering.
Criteria API, DTO projection, optional predicates, query bounds, deterministic ordering, and avoiding entity over-fetching.
Practice the problem like a real interview: restate, reason, implement, and test.
Implement an order-list query with optional status and customerId filters. Return only id, status, total, and , never entities. Results are ordered by and limited to at most 100 rows. An absent filter must not generate a fake predicate.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.
createdAtcreatedAt DESC, id DESCstatus=PAID, customerId=empty -> latest paid orders across customers
status=empty, customerId=42 -> latest orders for customer 42
both empty, limit=20 -> latest 20 orders
findAll and filter in Java.CriteriaBuilder.construct for the select shape.public record OrderListItem(
long id, OrderStatus status, java.math.BigDecimal total,
java.time.Instant createdAt) {}
@org.springframework.stereotype.Repository
class OrderListQuery {
@jakarta.persistence.PersistenceContext
private jakarta.persistence.EntityManager em;
java.util.List<OrderListItem> find(
java.util.Optional<OrderStatus> status,
java.util.Optional<Long> customerId, int requestedLimit) {
var cb = em.getCriteriaBuilder();
var query = cb.createQuery(OrderListItem.class);
var order = query.from(Order.class);
var predicates = new java.util.ArrayList<jakarta.persistence.criteria.Predicate>();
status.ifPresent(value -> predicates.add(cb.equal(order.get("status"), value)));
customerId.ifPresent(value -> predicates.add(cb.equal(order.get("customer").get("id"), value)));
query.select(cb.construct(OrderListItem.class,
order.get("id"), order.get("status"),
order.get("total"), order.get("createdAt")))
.where(predicates.toArray(jakarta.persistence.criteria.Predicate[]::new))
.orderBy(cb.desc(order.get("createdAt")), cb.desc(order.get("id")));
int limit = Math.max(1, Math.min(requestedLimit, 100));
return em.createQuery(query).setMaxResults(limit).getResultList();
}
}
Application-side work is O(f) for the small number of filters and O(r) to materialize returned rows. Database cost depends on selectivity and indexes; the result is bounded to 100 rows.
(:status is null or o.status = :status) everywhere can produce weaker plans on some databases.findAll moves database work and unbounded data into application memory.Spot a weak answer, missing edge case, or clearer explanation? Send it in.
Say thanks with a standalone, one-time $5 contribution. No account required.