Command Palette

Search for a command to run...

PHASE 2Beginner ~14 min· topic 26 of 28Enterprise

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.

0/28 · 0%

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.

DishRules.javawhole filejava
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. 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.java
    jshell> import static java.util.function.Predicate.not
    jshell> 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/negate and Predicate.not(...) (Java 11+) for in-memory rules; org.springframework.data.jpa.domain.Specification with JpaSpecificationExecutor.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.

terminal
$ kubectl top pod -l app=menu-admin
── what you'll see ──
NAME CPU(cores) MEMORY(bytes)
menu-admin-6c9f7d8b5-x2kqp 1890m 3712Mi
# and the pod restarts: OOMKilled

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 a WHERE tenant_id = ? in each method (see multi-tenancy in Phase 14).

Remember this

  1. 1

    A specification has one method, isSatisfiedBy(candidate), and combinators and, or, not that build bigger rules from small, named ones.

  2. 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. 3

    In Java, java.util.function.Predicate<T> already is a specification with and, or and negate. Spring Data JPA's Specification<T> builds JPA Criteria queries instead, so the rule runs in the database.

  4. 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

01

What makes a specification reusable?

02

Why might a rule exist in two forms, in memory and as a query?

Practice

01

Add highProtein() (a dish has a proteinGrams field ≥ 20) and search for high-protein dishes delivered to Pune.

02

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.

Back to phase