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 createdAt, never entities. Results are ordered by createdAt DESC, id DESC and limited to at most 100 rows. An absent filter must not generate a fake predicate.
status=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.Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.