When the predicate itself comes from data, allMatch over the rule stream is the filter. A Map of field name to accessor keeps the rules declarative instead of a switch that has to be edited for every new field.
record Rule(String field, String equalTo) {}
record Event(String type, String user, String region) {}
var rules = List.of(new Rule("type", "purchase"), new Rule("region", "east"));
var events = List.of(new Event("purchase", "u1", "east"),
new Event("purchase", "u2", "west"),
new Event("view", "u3", "east"));
Map<String, Function<Event, String>> readers =
Map.of("type", Event::type, "user", Event::user, "region", Event::region);
var matched = events.stream()
.filter(e -> rules.stream().allMatch(r -> readers.get(r.field()).apply(e).equals(r.equalTo())))
.toList();
System.out.println("rules : " + rules.size());
System.out.println("matched: " + matched);
rules : 2
matched: [Event[type=purchase, user=u1, region=east]]
Run this yourself in the Online Java Compiler, spin up a live REST API in the API Sandbox, or practise with Java interview questions.
Published 2026-09-20