Use Clojure keywords as self-evaluating labels for maps, statuses, options, qualified domain attributes, and data-first APIs.
Keywords are compact literal labels. They evaluate to themselves, compare efficiently, and work naturally as map keys.
1:status
2;; => :status
That self-evaluating behavior is the core difference from symbols. A symbol is usually resolved. A keyword is already the value.
| Use case | Example | Java comparison |
|---|---|---|
| Map keys | :user/email |
Field names or JSON property names |
| Status values | :active, :cancelled |
Enum constants |
| Options | {:mode :strict} |
Configuration constants |
| Event names | :order/submitted |
Event type constants |
| Spec or validation names | :user/id |
Qualified domain attribute names |
In Java, these concerns often require string constants, enums, getters, annotations, or wrapper classes. In Clojure, keywords provide a language-level literal for stable data vocabulary.
1(def user
2 {:user/id 42
3 :user/email "dev@example.com"
4 :user/status :active})
You can use a keyword as a function to look itself up in a map:
1(:user/email user)
2;; => "dev@example.com"
3
4(:user/role user :guest)
5;; => :guest
The optional third form above is a default value. get is also fine:
1(get user :user/email)
2;; => "dev@example.com"
Use the form that reads best in context. Keyword-as-function is common for simple lookups. get, get-in, and destructuring often read better when access is nested or dynamic.
Bare keywords like :id and :name are fine in small examples. Larger systems benefit from qualified keywords:
1{:user/id 42
2 :invoice/id 9001
3 :account/id 7}
The namespace part does not load code. It gives the data label durable context.
Qualified keywords help when:
id, name, or statusDouble-colon keywords use the current namespace or an alias:
1(ns my.app.user-service
2 (:require [my.app.user :as user]))
3
4::local-id
5;; => :my.app.user-service/local-id
6
7::user/id
8;; => :my.app.user/id
Use this when the namespace qualification should follow aliases instead of being repeated as a string-like prefix. This is common in specs, validation, and library APIs.
Keywords are often the simplest status representation:
1(defn transition [order]
2 (case (:order/status order)
3 :draft (assoc order :order/status :submitted)
4 :submitted (assoc order :order/status :paid)
5 :paid order
6 :cancelled order))
This is lighter than a Java enum, but it shifts discipline to the data model. A typo creates a different keyword. Centralize important state transitions and validate external inputs.
External systems usually speak strings:
Inside Clojure application code, keywords are often the better shape:
1(defn normalize-status [s]
2 (case s
3 "active" :active
4 "disabled" :disabled
5 "pending" :pending
6 :unknown))
Do the conversion deliberately at boundaries. Avoid calling keyword on arbitrary untrusted strings without a bounded vocabulary. It can create unbounded keyword values that are retained for the life of the process in many Clojure/JVM environments.
| Mistake | Better habit |
|---|---|
| Using strings as internal map keys by default | Use qualified keywords for stable domain attributes |
Using :id everywhere in a large system |
Prefer :user/id, :order/id, :invoice/id |
| Treating keyword namespaces as code namespaces | Remember they are data labels unless auto-resolved by :: |
| Creating keywords from unbounded input | Validate against known values first |
| Using keywords as function names | Use symbols for code names |
{"userId" 42 "status" "active"} as an internal Clojure map."active" and "disabled" strings to status keywords.:id and :name in a larger data shape with qualified keys.(:missing user :guest) and (get user :missing :guest).:: auto-resolves keyword namespaces through the current namespace or aliases.