Learn how to model the State pattern in Clojure with explicit states, transition functions, and atoms for coordinated current-state ownership instead of scattered conditional logic.
State pattern: A pattern where behavior changes according to an explicit current state instead of being spread across ad hoc conditionals.
In Clojure, the State pattern usually works best when the state is data and the transition rules are explicit. An atom can hold the current state when one evolving value needs synchronous ownership, but the real design value comes from the state machine model, not from the atom itself.
A useful starting point is a transition table or transition function:
1(def transitions
2 {:draft {:submit :review}
3 :review {:approve :approved
4 :reject :rejected}
5 :approved {}
6 :rejected {:revise :draft}})
This already expresses the core behavior: which events are legal in which states.
1(def workflow-state (atom :draft))
2
3(defn apply-event! [event]
4 (swap! workflow-state
5 (fn [current]
6 (if-some [next-state (get-in transitions [current event])]
7 next-state
8 (throw (ex-info "invalid transition"
9 {:state current :event event}))))))
The atom is just the holder of the current value. The important rule is that transition legality is explicit rather than buried in random cond branches across the system.
Without an explicit state model, the code often drifts toward:
Once state transitions live in one clear model, both tests and design review get easier.
stateDiagram-v2
[*] --> draft
draft --> review: submit
review --> approved: approve
review --> rejected: reject
rejected --> draft: revise
The value of this visual is that it exposes the legal lifecycle directly. If the team cannot agree on the diagram, it probably cannot agree on the code either.
Atoms are a good fit when:
Refs or event-sourced models may be better when:
Do not confuse “State pattern” with “use an atom everywhere.” The pattern is about explicit lifecycle control, not about one specific reference type.
This pattern goes bad when:
If the only thing an atom does is wrap a pile of hidden conditional logic, the design has not really improved yet.
Start with explicit states and transitions. Add an atom when a single current-state holder is useful. Keep transition validation close to the state model, and keep side effects outside the update function when possible.