Overreliance on Global State

Why too much shared mutable state makes Clojure systems harder to test, reason about, and evolve safely.

Overreliance on global state is an anti-pattern because it turns local reasoning into system-wide reasoning. In Clojure, this often happens when too much behavior depends on vars, root-level atoms, global config maps, or application-wide mutable containers that every namespace can touch.

The code may feel convenient at first. Later it becomes harder to test, harder to compose, and much more fragile under concurrency and change.

Anti-Pattern: Shared Mutable Root as a Shortcut

The classic version is a global atom or ref holding application state:

1(def app-state (atom {:users []
2                      :orders []
3                      :metrics {}}))

This can be acceptable for tiny tools or UI-style state containers. It becomes a problem when the rest of the system begins to depend on it everywhere.

Symptoms:

  • functions implicitly reach into the same mutable root
  • tests need elaborate setup and teardown
  • unrelated features contend on the same state boundary
  • behavior becomes difficult to isolate by subsystem

The anti-pattern is not one atom. It is letting one shared state root become the default answer for unrelated concerns.

Anti-Pattern: Hidden Inputs Through Vars

Global state makes function signatures look simpler while actually making behavior less explicit.

Weak:

1(def ^:dynamic *tax-rate* 0.13)
2
3(defn total-with-tax [subtotal]
4  (* subtotal (+ 1 *tax-rate*)))

The function appears to depend only on subtotal, but it really depends on ambient state too.

More explicit:

1(defn total-with-tax [subtotal tax-rate]
2  (* subtotal (+ 1 tax-rate)))

Now callers can see the dependency, tests become easier, and the function is easier to reuse.

The anti-pattern is hiding required inputs behind global availability.

Anti-Pattern: Global State as an Integration Layer

Sometimes global state becomes the accidental integration mechanism between parts of the application:

  • one namespace writes to it
  • another reads from it later
  • a third assumes a particular shape is already present

This creates loose-looking coupling that is actually very tight. The dependencies are hidden in data conventions rather than function contracts.

A clearer design usually passes explicit values, uses well-defined state owners, or exposes deliberate APIs around shared state boundaries.

Anti-Pattern: Pure-Looking Functions with Implicit Mutation

A function that appears ordinary but updates global state internally is much harder to reason about than its surface suggests.

Weak:

1(def audit-log (atom []))
2
3(defn approve-order [order]
4  (swap! audit-log conj {:event :approved :id (:id order)})
5  (assoc order :status :approved))

This mixes pure transformation with hidden mutation. That complicates:

  • testing
  • retries
  • reuse
  • composition

Often the better split is:

  • a pure function returning the updated order
  • an outer boundary that records the side effect deliberately

Anti-Pattern: Global State as the Fix for Parameter Passing

Sometimes global state is introduced mainly because explicit parameter passing feels verbose. That is understandable, but it often trades small call-site inconvenience for much larger long-term design cost.

In Clojure, passing data explicitly is usually not boilerplate. It is one of the main ways the code stays honest about what it depends on.

If too many parameters are genuinely awkward, the problem may be:

  • the function is doing too much
  • the domain model is poorly shaped
  • the subsystem boundary needs a better data structure

The anti-pattern is using global state to dodge those design questions.

Anti-Pattern: Test Suites That Depend on Global Reset Rituals

Global state often shows up as test pain:

  • tests must reset vars before each run
  • test order starts to matter
  • fixtures have to clean up hidden shared state
  • parallel test execution becomes unreliable

When test setup feels like state detox, the codebase is telling you something. The issue is often not the test framework. It is the application’s dependence on shared mutable context.

A Better State Boundary Model

    flowchart TD
	    A["Input data"] --> B["Pure transformation"]
	    B --> C["Explicit effect boundary"]
	    C --> D["State owner or integration layer"]

This model keeps most reasoning local. Functions stay about values. State changes happen in places that clearly own them. Dependencies become visible again.

When Global State Is Legitimate

This anti-pattern is not a claim that global state is always forbidden.

Some shared state is reasonable:

  • configuration loaded once at startup
  • carefully bounded caches
  • process-wide metrics collectors
  • REPL-time development helpers

The important distinction is whether the state is:

  • deliberately owned
  • operationally necessary
  • narrowly scoped

versus casually used as a convenience channel for everything.

What to Do Instead

  • pass dependencies explicitly where practical
  • keep core business functions pure when possible
  • isolate truly shared state behind well-defined owners
  • avoid giant mutable roots that mix unrelated concerns
  • let testability reveal where hidden global dependencies still exist

Common Mistakes

  • using one shared atom as the default system boundary
  • hiding real inputs in vars or root state
  • mixing pure-looking functions with hidden global mutation
  • using global state to avoid redesigning bad function boundaries
  • accepting brittle test reset rituals as normal

Key Takeaways

  • Global state reduces local reasoning and increases hidden coupling.
  • Convenience at the call site often becomes complexity elsewhere.
  • Explicit data flow is one of Clojure’s biggest design advantages.
  • Shared mutable state should be narrow, deliberate, and owned.
  • If tests keep fighting hidden state, the architecture probably needs refactoring.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026