State Pattern with Atoms and State Machines

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.

Model State as Data First

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.

Put the Current State Behind a Small Boundary

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.

Why This Is Better Than Scattered Conditionals

Without an explicit state model, the code often drifts toward:

  • duplicated transition checks
  • illegal transitions in some paths but not others
  • no shared place to see the lifecycle
  • hard-to-test behavior because every branch invents its own rules

Once state transitions live in one clear model, both tests and design review get easier.

The Lifecycle Becomes Visible

    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.

When Atoms Are the Right Holder

Atoms are a good fit when:

  • one independently evolving state value is being updated
  • transitions are synchronous
  • there is no need for multi-reference coordination

Refs or event-sourced models may be better when:

  • multiple states must change transactionally
  • history matters as much as the current state
  • coordination spans several references

Do not confuse “State pattern” with “use an atom everywhere.” The pattern is about explicit lifecycle control, not about one specific reference type.

Common Failure Modes

This pattern goes bad when:

  • states are represented inconsistently
  • transition rules are still duplicated elsewhere
  • side effects happen inside retryable state-update functions
  • state names exist, but no explicit transition model exists

If the only thing an atom does is wrap a pile of hidden conditional logic, the design has not really improved yet.

Practical Heuristics

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.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026