Product, FilterCriteria, CompositeFilter, and SortStrategy as the class model, using the Composite pattern to combine independent filters, and how this class design feeds into a full HLD-scale search engine.
Published September 23, 2026
class Product { String id; String name; String category; BigDecimal price; int stock; }
interface FilterCriteria { boolean matches(Product product); }
interface SortStrategy { Comparator<Product> comparator(); }
FilterCriteria as an interface (rather than a bundle of boolean flags checked in one giant method) is what lets individual filters — price range, category, in-stock — be defined, tested, and composed INDEPENDENTLY, each as its own small, focused implementation.
class CompositeFilter implements FilterCriteria {
private final List<FilterCriteria> filters;
CompositeFilter(List<FilterCriteria> filters) { this.filters = filters; }
public boolean matches(Product product) {
return filters.stream().allMatch(f -> f.matches(product)); // ALL filters must pass — AND semantics
}
}
class PriceRangeFilter implements FilterCriteria {
BigDecimal min, max;
public boolean matches(Product p) { return p.getPrice().compareTo(min) >= 0 && p.getPrice().compareTo(max) <= 0; }
}
CompositeFilter itself IMPLEMENTS FilterCriteria — this is the defining trait of the Composite pattern: a composite of filters is INDISTINGUISHABLE, from the caller's perspective, from a single filter. "Price range AND category AND in-stock" becomes simply new CompositeFilter(List.of(priceFilter, categoryFilter, stockFilter)), and code applying filters to products never needs to know or care whether it's dealing with one filter or many combined — it just calls matches().
class PriceAscending implements SortStrategy {
public Comparator<Product> comparator() { return Comparator.comparing(Product::getPrice); }
}
Keeping sort logic as its own Strategy (rather than baked into the filter or the query method) means adding a new sort order ("newest first," "best rating") is purely additive — implement one new class, no existing filter or query code needs to change, the same Open/Closed benefit that shows up throughout this course's Strategy Pattern discussions.
This class model is exactly the LOGICAL shape a real, HLD-scale search system (Search Engine, in the System Design track) needs to expose to callers — but at genuine scale, matches() being called against EVERY product in a naive linear scan doesn't work against millions of products. The HLD version replaces the linear scan with an INVERTED INDEX (Search Engine's core data structure) that can jump directly to candidate matches — but the FilterCriteria/CompositeFilter shape still describes the QUERY itself; it's the EXECUTION strategy underneath that changes from "scan and check" to "index and retrieve" as scale grows.
Q: How would you add OR semantics (price range OR category X) to this Composite design?
A: A second composite implementation, e.g. OrCompositeFilter, using anyMatch instead of allMatch — both implement the same FilterCriteria interface, and complex boolean expressions (mixing AND and OR) can be built by nesting composites of composites, exactly the recursive structure Composite pattern is named for.
Q: Does adding a new filter type (e.g. a 'brand' filter) require changing CompositeFilter?
A: No — this is the direct Open/Closed payoff: CompositeFilter only depends on the FilterCriteria interface, never on any specific implementation, so a new filter type is a new class implementing that interface, with zero changes needed to CompositeFilter or any existing filter.
Q: How would you handle a filter that's expensive to evaluate (e.g. checking real-time stock from an external inventory service)?
A: Order matters for performance — evaluating cheap, in-memory filters (price, category) FIRST and short-circuiting before reaching expensive filters (an external stock check) avoids unnecessary expensive calls for products that would've been filtered out anyway; allMatch's short-circuit behavior naturally supports this if filters are ordered cheap-to-expensive in the composite's list.
Q: Should sorting happen before or after filtering? A: Filtering first, then sorting — sorting the full unfiltered catalog before applying filters wastes work sorting products that filtering will discard anyway; filter down to the relevant subset first, then sort only that smaller result set.