Enterprise 3 — Specification
In one line
A Specification is a small object that answers "does this item satisfy a business rule?" and can be combined with and/or/not. The same rule can filter collections in memory or be translated into a database query.
Think of it like this
A recruiter's checklist: "5+ years of Java AND (lives in Pune OR open to relocation)". Each line is a reusable criterion, the recruiter combines them for each job, and the same checklist works whether screening paper CVs or searching a database.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Specification
- An object encapsulating one business rule that says whether an item satisfies it.
- Combinator
- An operation that builds a new rule from existing ones: and, or, not.
- Predicate
- Java's built-in function type
Predicate<T>: takes a value, returns true or false.
Step by step
01The problem: the same filters written five times
Tiffin's menu search, the "recommended for you" carousel and the offer eligibility check each re-implemented "vegetarian", "under ₹300" and "delivers to my city" with slightly different if conditions. A bug fixed in one copy stayed in the others.
02Name each rule once and combine them
Each rule becomes a named Predicate<Dish> in one place. Features combine them with and/or/negate, and a change to a rule's definition applies everywhere.
import java.util.*;
import java.util.function.Predicate;
record Dish(String name, boolean veg, long paise, Set<String> cities) {}
final class DishRules {
static Predicate<Dish> vegetarian() { return Dish::veg; }
static Predicate<Dish> priceBelow(long paise) { return d -> d.paise() < paise; }
static Predicate<Dish> deliversTo(String city) { return d -> d.cities().contains(city); }
}
final class Menu {
static final List<Dish> DISHES = List.of(
new Dish("Veg biryani", true, 22000, Set.of("Pune", "Mumbai")),
new Dish("Paneer roll", true, 14000, Set.of("Pune")),
new Dish("Chicken thali", false, 26000, Set.of("Pune", "Delhi")),
new Dish("Dal khichdi", true, 12000, Set.of("Delhi")));
static List<String> search(Predicate<Dish> spec) {
return DISHES.stream().filter(spec).map(Dish::name).toList();
}
}03Running the rule in the database
Filtering in memory is fine for small lists. For large tables the rule must become SQL. Spring Data JPA's Specification<Dish> returns a JPA Criteria Predicate: (root, query, cb) -> cb.isTrue(root.get("veg")). Specifications combine with .and()/.or(), and dishRepository.findAll(spec) turns the whole tree into one WHERE clause.
Keep the in-memory and database versions of a rule side by side and test them against the same examples, so they never disagree.
Try it yourself
- 1
Combine rules for two features
Search: vegetarian AND under ₹200 AND delivers to Pune. Carousel: delivers to Pune AND NOT vegetarian.
terminal$ jshell> /open DishRules.javajshell> import static java.util.function.Predicate.notjshell> Menu.search(DishRules.vegetarian().and(DishRules.priceBelow(20000)).and(DishRules.deliversTo("Pune")))jshell> Menu.search(DishRules.deliversTo("Pune").and(not(DishRules.vegetarian())))── expected output ──$5 ==> [Paneer roll]$6 ==> [Chicken thali]
In your stack
- →
Predicate.and/or/negateandPredicate.not(...)(Java 11+) for in-memory rules;org.springframework.data.jpa.domain.SpecificationwithJpaSpecificationExecutor.findAll(spec, pageable)for database rules.
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Filtering a whole table in memory
The admin search loads dishRepository.findAll() (2 million rows across all cities) and then applies the Predicate<Dish> specification in Java.
Myth vs fact
Myth
Specification needs its own class hierarchy.
Fact
In Java, Predicate<T> (in memory) and Spring Data's Specification<T> (in the database) already provide the pattern; you mostly write named factory methods.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Specifications are a good boundary for authorisation filters too: a
visibleTo(user)spec added to every query is safer than remembering aWHERE tenant_id = ?in each method (see multi-tenancy in Phase 14).
Remember this
- 1
A specification has one method,
isSatisfiedBy(candidate), and combinatorsand,or,notthat build bigger rules from small, named ones. - 2
Named rules (
isVegetarian,deliversTo(city),priceBelow(x)) make business logic readable and reusable across features: search, validation and eligibility checks all share them. - 3
In Java,
java.util.function.Predicate<T>already is a specification withand,orandnegate. Spring Data JPA'sSpecification<T>builds JPA Criteria queries instead, so the rule runs in the database. - 4
Specification is close to Interpreter (Behavioral 11): both are trees of combinable rules. Specification emphasises named domain rules in code; Interpreter emphasises rules stored as data and evaluated at runtime.
Explain it without notes
What makes a specification reusable?
Why might a rule exist in two forms, in memory and as a query?
Practice
Add highProtein() (a dish has a proteinGrams field ≥ 20) and search for high-protein dishes delivered to Pune.
Write the JPA Specification<Dish> version of priceBelow.
Trade-offs
- ↔
One definition per rule and readable combinations, versus keeping in-memory and database forms consistent, and the risk of loading too much data if the in-memory form is misused.
Done when you can
I can write named, combinable rules with Predicate.
I can explain when a rule must become a database query.