skip to content

How do you compose Specifications with and(), or(), and not() to build dynamic filters?

level: middleimportance: must knowfreq 60%

answer

  1. and() / or() instance, not() static
  2. where(null) neutral start, then chain
  3. allOf / anyOf skip nulls (3.1+)
  4. null predicate = dropped from AND/OR
  5. (a AND b) OR c precedence — group explicitly

basics

~10 s

Each small Specification is one condition. Combine them with spec1.and(spec2), spec1.or(spec2), and Specification.not(spec). You start from a neutral base and conditionally chain filters that are active, producing one combined WHERE clause.

solid answer

~40 s

Specification exposes default/static combinators: instance methods `and(other)` and `or(other)`, and the static `Specification.not(spec)`. You build dynamic filters by conditionally chaining only the specs whose inputs are present. A common pattern is to start from `Specification.allOf()`/`Specification.where(null)` (a neutral true) and `.and(...)` each active filter. In modern Spring Data JPA (3.x), `Specification.allOf(specs)` and `anyOf(specs)` accept a collection and AND/OR them together, ignoring null elements. `Specification.where(spec)` is a readability helper that just returns the spec (or a no-op). Because null predicates are treated as no restriction, an inactive filter can safely return a spec that yields null. This gives you clean, reusable predicate fragments that you assemble at runtime based on which search parameters the user actually supplied.

code

java · 20 lines
java
public final class UserSpecs {
    private UserSpecs() {}

    public static Specification<User> hasStatus(UserStatus s) {
        return (root, q, cb) -> s == null ? null : cb.equal(root.get("status"), s);
    }
    public static Specification<User> emailContains(String frag) {
        return (root, q, cb) -> frag == null ? null
                : cb.like(cb.lower(root.get("email")), "%" + frag.toLowerCase() + "%");
    }
    public static Specification<User> createdAfter(Instant t) {
        return (root, q, cb) -> t == null ? null : cb.greaterThan(root.get("createdAt"), t);
    }
}

// AND (a AND b) OR c grouping made explicit:
Specification<User> spec = UserSpecs.hasStatus(ACTIVE)
        .and(Specification.anyOf(UserSpecs.emailContains("@corp"),
                                  UserSpecs.createdAfter(cutoff)));
List<User> result = userRepository.findAll(spec);

go deeper

for a junior

Know and()/or() chain conditions and not() negates.

for a middle

Build a real dynamic-filter method; understand null-as-no-restriction and allOf/anyOf.

for a senior

Handle AND/OR precedence correctly and design reusable null-safe factories.

for a principal

Set guardrails against unbounded all-null queries; standardize a filter-composition convention across the codebase.

**The combinators.** - `spec.and(other)` — logical AND of two specifications; instance default method. - `spec.or(other)` — logical OR. - `Specification.not(spec)` — static; negates a specification. - `Specification.where(spec)` — static readability entry point; historically wrapped a nullable spec so you could start a chain. It simply returns the given spec (or a neutral one if null). - `Specification.allOf(Iterable/varargs)` (Spring Data JPA 3.1+) — ANDs many specs, skipping nulls. Returns a neutral (always-true) spec if the collection is empty. - `Specification.anyOf(Iterable/varargs)` — ORs many specs, skipping nulls. **Null handling — the key mechanic.** When a composed spec's `toPredicate` returns `null`, Spring treats it as "no restriction" and drops it from the AND/OR. This lets you write optional filters that simply contribute nothing when their input is absent, so callers don't need if/else around every `.and()`. Note: this null-skipping applies to composition; if the *entire* combined specification resolves to null, the query has no WHERE clause (returns everything). **Dynamic-filter pattern.** ``` public List<User> search(UserFilter f) { Specification<User> spec = Specification.where(null); if (f.getStatus() != null) spec = spec.and(UserSpecs.hasStatus(f.getStatus())); if (f.getEmailLike() != null) spec = spec.and(UserSpecs.emailContains(f.getEmailLike())); if (f.getCreatedAfter() != null) spec = spec.and(UserSpecs.createdAfter(f.getCreatedAfter())); return repo.findAll(spec); } ``` Or with a collection and `allOf`, letting each factory return null when inactive: ``` Specification<User> spec = Specification.allOf( UserSpecs.hasStatus(f.getStatus()), // returns null-yielding spec if status null UserSpecs.emailContains(f.getEmailLike()) ); ``` **Precedence gotcha.** `a.and(b).or(c)` groups left-to-right: `(a AND b) OR c`. If you need `a AND (b OR c)`, build the OR sub-spec first: `a.and(b.or(c))` or `a.and(Specification.anyOf(b, c))`. Getting AND/OR grouping wrong is a classic bug. **Deprecation note.** In Spring Data JPA 3.x, `Specification` became a *pure* functional interface and its combinators (`and`/`or`/`not`/`where`/`allOf`/`anyOf`) are all present; some earlier 2.x nullable overloads of `where`/`and`/`or` accepting `@Nullable` were tidied. Behavior of null-as-no-restriction is preserved. **Reusability.** Because each factory returns a standalone `Specification`, the same `hasStatus` fragment can be reused in count(), exists(), delete(), and different composed queries — the core benefit over duplicating conditions across many derived-query methods. **Thread-safety.** Specifications are effectively immutable descriptions; composing produces new specs, so they're safe to build and share.

  • How would you express 'status = ACTIVE AND (email contains X OR createdAfter Y)'?
    Build the OR first, then AND it: hasStatus(ACTIVE).and(emailContains(X).or(createdAfter(Y))), or use hasStatus(ACTIVE).and(Specification.anyOf(emailContains(X), createdAfter(Y))). Chaining a.and(b).or(c) would wrongly give (a AND b) OR c.
  • What happens if every filter is inactive and each returns a null predicate?
    The combined specification resolves to null / no WHERE clause, so findAll returns all rows. Guard against unbounded queries (e.g. always require a page size or a mandatory filter).

saying these in an interview costs you the question

  • Assuming a.and(b).or(c) means a AND (b OR c) — it's (a AND b) OR c.
  • Wrapping every filter in manual if/else instead of returning null for inactive predicates.
  • Thinking not() is an instance method — Specification.not(spec) is static.
  • Forgetting that all-null specs yield an unbounded query (no WHERE).

context