Real World Example

Myntra-style Filter Sidebar

Fashion apps like Myntra and Ajio have a sidebar with category, brand, price range, discount and "in stock" filters, any combination of which can be active. Behind it: a Specification-based search returning a DTO page, plus a projection query for the filter counts ("Brand: Puma (124)").

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CatalogSearchService {

    private final ProductRepository products;
    private final EntityManager em;

    public Page<ProductCardDto> search(SearchRequest r, Pageable pageable) {
        Specification<Product> spec = Specification.allOf(Stream.of(
                r.category() == null ? null : ProductSpecs.inCategory(r.category()),
                r.brands().isEmpty() ? null : ProductSpecs.brandIn(r.brands()),
                r.min() == null      ? null : ProductSpecs.priceAtLeast(r.min()),
                r.max() == null      ? null : ProductSpecs.priceAtMost(r.max()),
                r.inStock()          ? ProductSpecs.inStock() : null)
            .filter(Objects::nonNull).toList());
        return products.findAll(spec, pageable).map(ProductCardDto::from);
    }

    public List<FacetCount> brandFacets(String category) {
        return em.createQuery("""
                select new com.shopease.catalog.dto.FacetCount(p.brand, count(p))
                from Product p
                where p.category.name = :category and p.stock > 0
                group by p.brand
                order by count(p) desc
                """, FacetCount.class)
            .setParameter("category", category)
            .getResultList();
    }
}

public record FacetCount(String value, long count) {}

The Response

Example

curl "localhost:8080/api/catalog/search?category=Shoes&brands=Puma,Nike&max=4000&inStock=true&size=2&sort=price,desc"

Example output

HTTP/1.1 200 OK
Content-Type: application/json

{
  "content": [
    {"id": 311, "name": "Puma Softride Running Shoes", "price": 3999.00},
    {"id": 288, "name": "Nike Revolution 6", "price": 3695.00}
  ],
  "page": {"size": 2, "number": 0, "totalElements": 37, "totalPages": 19},
  "facets": {"brand": [{"value": "Puma", "count": 124}, {"value": "Nike", "count": 98}]}
}

Which Query Tool for Which Job

NeedTool
Simple lookup by one or two fieldsderived query method
Fixed query with joins/aggregates@Query JPQL
Read-only list for an APIDTO / interface projection
Optional, combinable filtersSpecifications (or Criteria API)
Database-specific SQL, reportsnative query
Bulk change@Modifying update/delete