Why unchecked inputs make Clojure systems brittle, and how specs or other validation boundaries prevent invalid data from leaking inward.
Spec: In Clojure, a declarative description of the shape or predicate constraints of data that can be used for validation, explanation, instrumentation, and test-data generation.
Neglecting spec and data validation is a Clojure anti-pattern because dynamic systems stay robust only when data boundaries are made explicit. If untrusted or loosely shaped data flows deep into the application before anyone checks it, the eventual failure becomes harder to diagnose and easier to mis-handle.
This is not an argument that every local value needs a spec. It is an argument that important boundaries need a contract.
One of the most common failures is letting data from the outside world travel too far before validation:
Weak:
1(defn create-user! [req]
2 (persist-user! (:body req)))
If :body is malformed, the failure may appear much later in persistence, business rules, or rendering code.
The anti-pattern is not “missing spec syntax.” It is missing a validation boundary near the point where the system stops controlling the shape of the data.
Sometimes teams validate only after doing real work with the data.
That leads to:
A better pattern is boundary validation first, then ordinary domain logic on known-good data.
Specs are useful because they describe and validate data. They are weaker when they exist only as comments-in-code and the runtime path never actually checks them where it matters.
For example, defining:
1(s/def ::email string?)
does not help much if the boundary never validates incoming email values before downstream functions assume more than “is a string.”
The anti-pattern is believing the presence of a spec definition alone has made the system safer.
The opposite mistake is also real: validating every tiny intermediate value just because specs exist.
That can create:
A better rule is:
The anti-pattern is not “too much safety” in the abstract. It is validation placed without regard for ownership and cost.
One of the best things about spec is not just s/valid?. It is the ability to explain why data failed the contract.
Weak:
1(if (s/valid? ::user payload)
2 ...
3 (throw (ex-info "invalid user" {})))
Stronger:
1(when-not (s/valid? ::user payload)
2 (throw
3 (ex-info "Invalid user payload"
4 {:type :user/invalid
5 :problems (s/explain-data ::user payload)})))
The anti-pattern is validating and then discarding the details that would make debugging and user feedback practical.
Specs and predicate-based validation are excellent for structure and low-level constraints:
They do not automatically replace all domain rules.
Example:
:start-date is present and parseable” is validation:start-date must be before :end-date unless this workflow is draft” is business logicThe anti-pattern is assuming one validation layer replaces all higher-level policy reasoning.
Some functions are effectively mini-APIs:
If those functions accept rich maps and never declare or validate the expected shape, callers guess. That guesswork tends to surface later as nil-punning, ad hoc key checks, and brittle implicit contracts.
Spec is one strong option here, especially when you want:
The deeper point is not tool loyalty. It is explicit contract ownership.
flowchart TD
A["Untrusted or loosely shaped input"] --> B["Validate at boundary"]
B --> C{"Valid?"}
C -->|No| D["Return explainable failure"]
C -->|Yes| E["Convert to trusted domain data"]
E --> F["Run domain logic with fewer defensive checks"]
This keeps the rest of the system cleaner because the shape uncertainty is handled where it enters.
s/explain-data or equivalent detail when failures must be diagnosable