Extra pattern · not GoF
Specification pattern
A specification is a boolean rule object — “is satisfied by this candidate?” — that you can combine with and, or, and not. Use it when rules grow and nest; keep simple checks as plain ifs.
Example: the lot now has EV-only, handicapped, compact, and “not overnight” rules. Someone starts adding booleans to allocate(vehicle, evOnly, handicap, compact, overnight). Named specification objects compose without a boolean soup parameter list.
Can I combine this rule with another without adding a boolean?
Read the diagram definition
flowchart TB
A[allocate vehicle] --> S[Specification: EV and fits and free]
S --> C[candidates]
C --> T[AllocationStrategy.choose]
T --> SpotRules you can and together
interface SpotSpec {
boolean isSatisfiedBy(ParkingSpot spot);
default SpotSpec and(SpotSpec other) {
return spot -> this.isSatisfiedBy(spot) && other.isSatisfiedBy(spot);
}
default SpotSpec or(SpotSpec other) {
return spot -> this.isSatisfiedBy(spot) || other.isSatisfiedBy(spot);
}
default SpotSpec not() {
return spot -> !this.isSatisfiedBy(spot);
}
}
class EvOnly implements SpotSpec {
public boolean isSatisfiedBy(ParkingSpot spot) { return spot.isEv(); }
}
class FitsVehicle implements SpotSpec {
private final Vehicle vehicle;
FitsVehicle(Vehicle vehicle) { this.vehicle = vehicle; }
public boolean isSatisfiedBy(ParkingSpot spot) { return spot.fits(vehicle); }
}
SpotSpec forCar = new EvOnly().and(new FitsVehicle(vehicle));Specification is not Strategy
Specification
Which spots are even . A with a name. Filter first.
Strategy
How to choose among spots that already fit — nearest, first-free, random. See Strategy.
You often use both. A policy is a cousin: one named business rule, often not composed. Specification is the compose story. A rule engine case study is this pattern at product size.
Smell
Boolean parameter explosion
Growing method
allocate(v, ev, handicap,
compact, overnight)You will miss a combination. The next rule is another flag.
Named rules
EvOnly.and(FitsVehicle)
.and(NotReserved)Compose the ones the use case needs. A new rule is a new class.
Keep SQL out of the spec
isSatisfiedBy should talk to the domain object. If every spec becomes a WHERE clause, you coupled rules to the database. Filter in memory for LLD, or map the spec at the repository — do not hide JDBC in the rule. That is the same boundary as pagination and filtering: the domain names the predicate; persistence may translate it later.
class Allocator {
Optional<ParkingSpot> choose(List<ParkingSpot> spots, SpotSpec spec, AllocationStrategy strategy) {
List<ParkingSpot> candidates = spots.stream()
.filter(spec::isSatisfiedBy)
.toList();
return strategy.choose(candidates);
}
}Composite is the tree cousin
Composite treats a leaf and a group as the same type. AndSpec / OrSpec are composites of predicates. You do not need to say both names in a round. Say “rules I can and together” and show the interface.
Boolean parameter explosion
allocate(v, ev, handicap, compact, overnight) will miss a combination. Name each rule. Compose the ones the use case needs.
Putting SQL inside the spec
isSatisfiedBy should talk to the domain object. If every spec becomes a WHERE clause, you coupled rules to the database. Filter in memory for LLD, or map the spec at the repository.
or() and not() are easy to get wrong
EvOnly.or(HandicapOnly) is a union of candidates. EvOnly.and(HandicapOnly.not()) is “EV and not reserved for access.” Say the English sentence before you chain methods. A wrong and/or is a silent empty lot.
Library eligibility is the same pattern
Library rules — member in good standing, copy available, not a reference-only title — combine. A boolean checkout(member, copy, goodStanding, available, reference) will miss a combination. Name each spec.
interface LoanSpec {
boolean isSatisfiedBy(LoanRequest request);
}
class MemberInGoodStanding implements LoanSpec {
public boolean isSatisfiedBy(LoanRequest r) { return !r.member().isBlocked(); }
}
class CopyAvailable implements LoanSpec {
public boolean isSatisfiedBy(LoanRequest r) { return r.copy().isOnShelf(); }
}Worked filter: EV that fits, not reserved
The use case is “park an EV.” The spec is EvOnly.and(FitsVehicle).and(new NotReserved()). The strategy then picks nearest or first-free among candidates. A handicapped-only follow-up is a new class plus one and(), not a new boolean on allocate().
class NotReserved implements SpotSpec {
public boolean isSatisfiedBy(ParkingSpot spot) { return !spot.isReserved(); }
}
class HandicapOnly implements SpotSpec {
public boolean isSatisfiedBy(ParkingSpot spot) { return spot.isHandicap(); }
}
SpotSpec evJob = new EvOnly().and(new FitsVehicle(vehicle)).and(new NotReserved());
SpotSpec accessJob = new HandicapOnly().and(new FitsVehicle(vehicle)).and(new NotReserved());When one or two ifs are enough
If the lot only has “must fit” and “must be free,” write two ifs. Specification is worth using when rules combine and the interviewer will add a third. Do not extract a framework for a boolean that will never grow. That is the same instinct as YAGNI.