Choose symbols for code-facing names and keywords for data-facing labels, with Java comparisons, map examples, review heuristics, and common migration mistakes.
If this subsection only gives you one lasting habit, make it this:
Symbols are resolved names. Keywords are literal data labels.
That distinction makes Clojure syntax much easier to read.
| Question | Symbol | Keyword |
|---|---|---|
| Does it usually evaluate to itself? | No | Yes |
| Does it usually participate in name lookup? | Yes | No |
| Does it usually name code? | Yes | No |
| Does it usually label data? | No | Yes |
| Common examples | user, map, str/trim |
:user/id, :active |
| Java analogy | Local name, method reference, class name | Field label, enum value, constant token |
Example:
1(defn active-emails [users]
2 (->> users
3 (filter #(= :active (:user/status %)))
4 (map :user/email)))
In this form:
defn, active-emails, users, filter, map, and = are symbols.:active, :user/status, and :user/email are keywords.The symbols tell Clojure what code to run. The keywords describe the data.
A symbol resolves:
1(def threshold 10)
2
3threshold
4;; => 10
A keyword evaluates to itself:
1:threshold
2;; => :threshold
Quoted symbols are the exception because quoting stops resolution:
1'threshold
2;; => threshold
Java comparison is useful but imperfect:
| Java habit | Clojure equivalent tendency |
|---|---|
| Local variables and parameters | Symbols |
| Method or static references | Symbols or qualified symbols |
| Field names in DTO-like data | Qualified keywords |
| Enum constants | Keywords |
| String constants for keys | Keywords inside the Clojure core |
| Reflection over names | Rare; usually macros or tooling |
The biggest shift is that Clojure data is often plain maps. Keywords make those maps readable without creating classes just to hold fields.
Good symbol use cases:
let bindings1(require '[clojure.string :as str])
2
3(let [email "DEV@EXAMPLE.COM"]
4 (str/lower-case email))
email and str/lower-case are symbols because Clojure resolves them.
Good keyword use cases:
1{:event/type :user-registered
2 :user/id 42
3 :user/email "dev@example.com"}
Everything with a leading : is a literal data label.
When reviewing code, ask:
| Review question | Likely answer |
|---|---|
| Should this token be looked up in scope or a namespace? | Symbol |
| Should this token stay as a literal value in data? | Keyword |
| Is this a map key in internal domain data? | Usually keyword |
| Is this a function, parameter, or local name? | Symbol |
| Is this external JSON or HTTP data? | String at boundary, keyword after normalization |
flowchart TD
A["Identifier-like token"] --> B{"Leading colon?"}
B -->|Yes| C["Keyword: data label"]
B -->|No| D{"Quoted?"}
D -->|Yes| E["Symbol as data"]
D -->|No| F{"Names code?"}
F -->|Yes| G["Symbol"]
F -->|No| H["Reconsider data shape"]
| Mistake | Why it hurts | Better choice |
|---|---|---|
{user/id 42} instead of {:user/id 42} |
Tries to resolve user/id as a symbol |
Use a keyword map key |
{"status" "active"} everywhere internally |
Stringly typed core data | Normalize to {:status :active} |
:email as a function parameter |
Keywords do not introduce locals | Use email as the symbol, :email as the map key |
| Dynamically creating symbols for fields | Adds metaprogramming where data lookup is enough | Use keyword keys |
Calling keyword on arbitrary input |
Can create unbounded retained keyword values | Validate and map known values |
1(defn paid? [invoice]
2 (= :paid (:invoice/status invoice)))
1{"userId" 42
2 "status" "active"}
user/id and :user/id are not the same token."active" to :active and rejects unknown statuses.