Specification
Encapsulate a business rule as an object with an isSatisfiedBy(candidate) method, instead of scattering boolean logic through the codebase, so rules can be named, tested alone, and combined with AND/OR/NOT.
The problem
LoanUnderwriter.isEligible() checks credit score, bankruptcy history, and debt-to-income
ratio in one long boolean expression. It works, right up until a second product - a
promotional card with slightly different thresholds - needs almost, but not exactly, the
same logic.
Copy the expression and tweak two numbers, and now the two rules drift independently every
time someone edits one and forgets the other. The underlying business rules were never
actually separate things; they were fused into whichever if statement happened to need
them first.
The solution
Give each atomic business rule its own object with one method: isSatisfiedBy(candidate).
MinimumCreditScoreSpec knows about credit scores and nothing else. NoRecentBankruptcySpec
knows about bankruptcy history and nothing else. Neither has any idea the other exists.
Then let specifications combine. An AndSpecification wraps two specifications and requires
both to pass - and because it implements the exact same interface it wraps, you can build an
AndSpecification out of two other composites just as easily as out of two atomic rules.
The eligibility rule for a whole product becomes one line built from named, independently
testable pieces.
- 1Two independent rules get combined into one at setup time, without either rule knowing the other exists.
- 2The underwriter asks the combined rule a single yes/no question about one applicant.
- 3AndSpecification delegates to its first child first.
- 4The rule checks only what it was built to check - nothing about bankruptcy history.
- 5Since the first rule passed, AndSpecification checks the second one too.
- 6This rule is equally reusable on its own - a different product could check it alone.
- 7Both children passed, so the combined rule reports true. The underwriter never wrote an AND by hand.
Structure
AndSpecification both implements Specification and has two of them. That double
relationship - the same shape appearing as both the whole and its parts - is what lets rules
nest to any depth without new code.
Code
Same example three ways: a loan eligibility check built from two independently testable rules.
When to use it
- The same eligibility, filtering, or validation logic needs to run in more than one place (a list filter, a form validator, a discount gate) and copies are already drifting apart.
- Rules need to be combined differently for different contexts (a stricter version for one product, a looser one for another) without duplicating the atomic checks themselves.
Pitfalls
- Ceremony without reuse. Wrapping every single-use
ifin its own class buys nothing. The pattern pays off once a rule is reused, tested alone, or recombined - not before. - Hidden performance cost. A deep tree of specifications evaluated per row over a large in-memory collection can be noticeably slower than one inlined boolean expression; profile before assuming it is free.
- Specifications that reach outside the candidate. A rule that queries a database or
calls a network service inside
isSatisfiedBy()stops being a pure predicate and gets much harder to test or combine safely.
Don't confuse it with
- Strategy. Both hide a piece of logic behind one interface method. Strategy is chosen to swap an algorithm that produces a result; Specification evaluates a yes/no predicate and is explicitly built to combine with AND/OR/NOT, which Strategy objects are not.
- Chain of Responsibility. CoR passes one request along a chain until a single handler acts and the chain stops; a composite Specification evaluates every child every time and combines their booleans - nothing "handles and stops" the rest.
- Validation frameworks. Framework annotations like
@NotNullor@Minsolve the same surface problem for simple field checks, but Specification is meant for genuine business rules that combine, get named, and get reused across unrelated validation, filtering, and gating contexts.
Check yourself
What is the single method every Specification, atomic or composite, must implement?