Learn how to separate request state, identity state, cache state, client state, and durable business state in Clojure web systems without turning services into hidden mutable tangles.
State management: The discipline of deciding which facts live where, how long they remain authoritative, and which part of the system is allowed to change them.
State is where many web systems stop being simple. Not because Clojure makes state hard, but because HTTP, browsers, caches, sessions, and databases all tempt teams to store the same information in too many places at once.
The stronger design habit is to classify state before you implement it.
Most web applications contain several very different kinds of state:
Those categories should not all be stored in the same place.
The visual below shows the healthier shape: a mostly stateless web layer backed by explicit external stores with different responsibilities.
The most important rule is simple: authoritative domain data should live in durable storage, not in cookies, not in local storage, and not in a process-local atom inside the web service.
That means:
If a fact must survive deploys, restarts, and multi-instance scaling, it belongs in the primary data store or another explicitly durable subsystem.
This sounds obvious, but many web failures come from violating it indirectly. Teams put authoritative workflow flags into Redis, recreate them from incomplete events, cache partially computed permissions, or let browser state drift into server truth. The bug is often architectural before it is technical.
A session answers questions like:
That is different from storing mutable business objects in the session itself. Sessions are good for identity continuity. They are usually a bad place for large or authoritative domain aggregates.
Token-based approaches such as JWTs can reduce server-side session storage, but they do not remove the need for server-side authority. Roles, permissions, and revocation-sensitive decisions often still need a trusted backend check.
The key distinction is:
Those are related, but they should not collapse into the same storage model.
Caches improve latency and reduce load, but they do not define correctness. A cache is strongest when the team can answer two questions clearly:
If those answers are vague, the cache often becomes the beginning of stale-data bugs.
In Clojure services, caches usually work best for:
They are dangerous when used as an undocumented substitute for persistence.
The more a cache participates in correctness, the more it starts acting like a database without database discipline. If stale or missing cache data can create broken decisions, the team usually needs a clearer source of truth, invalidation contract, or read model.
Browser code often needs local state for forms, selection state, sort order, modal visibility, or optimistic pending changes. That is normal and healthy.
The mistake is assuming that because the browser currently holds a value, it is therefore the authoritative value. Client state exists to support the user interaction model. It does not replace server-side validation, server-side authorization, or durable persistence.
This is especially important in richer ClojureScript front ends. Shared language and shared validation helpers are useful, but the browser is still an untrusted runtime. Great UX can validate early and keep interactions fast without pretending the client is the authority.
Clojure makes explicit state containers such as atoms, refs, and agents easy to use. That is helpful, but in web systems it also creates a temptation to store process-local mutable state that quietly breaks under scaling or redeploys.
An atom can be fine for:
It is usually the wrong choice for:
Even when local mutable state is technically safe, it needs a lifecycle story. Ask:
If those answers are fuzzy, the design is relying on accidental locality.
When placing a piece of state, classify it on three axes:
That usually gives better answers than starting from tools such as Redis, cookies, or atoms.
For example:
Once those classifications are explicit, arguments about storage technology become much easier.
When deciding where a piece of state should live, ask:
Those questions usually make the storage choice clearer than starting with a favorite tool such as Redis, local storage, or an atom.
Identity context belongs there. Mutable domain state usually does not.
If a stale cache entry can break correctness, the invalidation strategy is probably underdesigned.
What works on one node with one REPL session often fails the moment the service scales horizontally.