Learn how the Strategy pattern maps naturally to higher-order functions in Clojure, when plain function arguments are enough, and when data-driven or protocol-based dispatch is a better fit.
Strategy pattern: A way to choose one behavior from a family of interchangeable behaviors without hard-coding every choice into the caller.
The Strategy pattern maps unusually well to Clojure because functions are already first-class values. In many cases, the “pattern” is simply passing the behavior you want as an argument. That makes Clojure’s version smaller and more honest than the class-heavy form usually shown in object-oriented examples.
If the only variation is “which algorithm should run,” a higher-order function is often the whole solution:
1(defn charge-order [pricing-fn order]
2 (pricing-fn order))
3
4(defn standard-pricing [{:keys [subtotal]}]
5 {:total subtotal})
6
7(defn discounted-pricing [{:keys [subtotal vip?]}]
8 {:total (if vip?
9 (* subtotal 0.9M)
10 subtotal)})
The caller chooses the behavior by selecting the function. That keeps the variation explicit and avoids inventing unnecessary indirection.
Strategy gets more interesting when the algorithm choice has business meaning:
1(defn low-risk-score [{:keys [country new-device?]}]
2 (if (and (= country "CA") (not new-device?)) 5 20))
3
4(defn strict-risk-score [{:keys [country new-device? failed-logins]}]
5 (+ (if (= country "CA") 5 20)
6 (if new-device? 30 0)
7 (* failed-logins 10)))
8
9(defn classify-login [risk-fn login]
10 (let [score (risk-fn login)]
11 (cond
12 (< score 20) :allow
13 (< score 50) :challenge
14 :else :deny)))
The policy changes, but the caller’s workflow does not. That is the pattern doing useful work rather than just demonstrating flexibility for its own sake.
When strategy choice is determined by configuration or input data, a map can keep the selection logic cleaner:
1(def pricing-strategies
2 {:standard standard-pricing
3 :discounted discounted-pricing})
4
5(defn price-order [strategy-key order]
6 (if-some [strategy (get pricing-strategies strategy-key)]
7 (strategy order)
8 (throw (ex-info "unknown pricing strategy"
9 {:strategy strategy-key}))))
This makes the available strategies inspectable and easy to test. It also works well when the chosen behavior needs to come from configuration, feature flags, or request metadata.
Plain functions stop being enough when the variation depends on more than one concern:
At that point, a protocol, multimethod, or data-plus-functions shape may be more honest than forcing everything into one function argument.
The visual below shows the main difference between hard-coded branching and a strategy-oriented design.
flowchart LR
A["Caller"] --> B{"Choose behavior"}
B --> C["Strategy A"]
B --> D["Strategy B"]
B --> E["Strategy C"]
C --> F["Shared result path"]
D --> F
E --> F
The main point is not abstraction for its own sake. It is that the caller delegates the variable part without needing to know the internal details of each algorithm.
Strategy goes wrong when:
case would have been clearerAnother common mistake is wrapping tiny one-off functions in ceremony just to claim the pattern is present. In Clojure, the advantage is that the pattern can stay small.
Reach for Strategy when the variation is real, stable, and caller-visible. Use plain functions first. Move to maps, protocols, or multimethods only when the complexity of selection or extension genuinely demands it.