Learn how to model Specification in Clojure with composable predicates, rule combinators, and data-driven criteria instead of hard-coded nested conditionals.
Specification pattern: A pattern for expressing business rules as reusable criteria that can be combined, tested, and applied consistently.
The Specification pattern fits Clojure well because predicates are already composable values. Instead of burying complex criteria inside nested if or cond forms, you can expose the rules directly as functions and then combine them with clear logical composition.
1(defn active-customer? [{:keys [status]}]
2 (= status :active))
3
4(defn credit-ok? [{:keys [credit-score]}]
5 (>= credit-score 700))
6
7(defn region-supported? [{:keys [region]}]
8 (contains? #{:ca :us} region))
Named predicates already improve the design because they make the criteria visible and testable on their own.
1(def eligible-for-premium?
2 (every-pred active-customer? credit-ok? region-supported?))
That one line communicates more clearly than a long nested conditional because the rule components are first-class and readable in isolation.
Sometimes a boolean answer is not enough. You may need to know why a value failed:
1(defn premium-failures [customer]
2 (cond-> []
3 (not (active-customer? customer)) (conj :inactive)
4 (not (credit-ok? customer)) (conj :credit-too-low)
5 (not (region-supported? customer)) (conj :unsupported-region)))
This is still specification-oriented design, but now the result carries diagnostic meaning. That matters for workflows such as review queues, rejection messaging, or audit trails.
If the criteria need to be configured, a data-driven representation may be better than hard-coded functions:
1(def premium-policy
2 {:status #{:active}
3 :min-credit-score 700
4 :regions #{:ca :us}})
The evaluation logic can then stay generic while the policy data changes independently. That is often a better long-term design for business rules than scattering literal thresholds through application code.
The visual below shows the core move: separate rule pieces, then combine them deliberately.
flowchart LR
A["Rule 1"] --> D["Combined specification"]
B["Rule 2"] --> D
C["Rule 3"] --> D
D --> E["Decision"]
The point is not abstraction for its own sake. It is that complex criteria become easier to inspect, test, and change.
Specification goes wrong when:
The pattern should reduce hidden logic, not multiply symbolic noise.
Use Specification when the rules have business meaning, need reuse, or are likely to evolve. Start with named predicates. Add combinators or data-driven policies when the rule set becomes large enough to justify them.