2.3 Filter / Criteria
The Filter pattern (also called Criteria) has a plain idea: make every filter rule its own object, then let those objects combine freely to select a subset that satisfies the conditions.
It is not one of the 23 classic GoF patterns, but it is extremely common in business systems — any "query/filter by multiple conditions" scenario uses it. Its essence is the same as "predicate composition" in functional programming.
The filter below lets you toggle several criteria objects and watch the result set shrink in real time.
Turn conditions into objects
interface Criteria {
List<Person> meet(List<Person> people);
}
class CriteriaMale implements Criteria {
public List<Person> meet(List<Person> people) {
return people.stream().filter(Person::isMale).toList();
}
}Each Criteria can be tested in isolation and reused independently. That is exactly why it is more maintainable than one long chain of inline ifs.
Compose with And / Or / Not
The real power: the combinators are themselves Criteria.
class AndCriteria implements Criteria {
private Criteria a, b;
public List<Person> meet(List<Person> people) {
return b.meet(a.meet(people)); // filter by a, then by b
}
}So a nested expression like new AndCriteria(male, new OrCriteria(admin, vip)) describes "male AND (admin OR vip)." The composer below lets you switch AND/OR/NOT and watch the result set change with the boolean logic.
Relationship to modern language features
In languages with first-class functions, the Filter pattern often "degrades" into a lighter form: a Predicate<T> plus and(), or(), negate(). Java's Stream.filter(Predicate), C#'s LINQ, and JavaScript's Array.filter are all language-level expressions of the same idea.
Understanding the pattern helps you choose between "writing a pile of classes" and "passing a lambda": make objects when rules are complex and need reuse and testing; use a lambda for a one-off simple check.