Specification Pattern for Complex Criteria

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.

Start with Small Named Predicates

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.

Compose Them into Larger Specifications

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.

When Specifications Need Explanation

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.

Data-Driven Specifications Can Scale Better

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 Main Benefit

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.

Common Failure Modes

Specification goes wrong when:

  • every tiny predicate becomes its own abstraction with no real reuse
  • the combined rule is harder to understand than a direct condition
  • booleans are used where richer failure information is needed
  • thresholds and policies are still duplicated elsewhere

The pattern should reduce hidden logic, not multiply symbolic noise.

Practical Heuristics

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.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026