Learn the narrow cases where a Clojure macro is justified: evaluation control, binding forms, control-flow syntax, and small DSLs whose expansions remain readable.
A good macro earns its complexity by giving callers a syntax shape that functions cannot provide. If the only benefit is shorter code, keep looking for a function.
The strongest macro use cases are practical and narrow:
| Use case | Why a function may not be enough |
|---|---|
| Control whether body forms evaluate | Function arguments evaluate before the function runs. |
| Introduce a binding or resource scope | The call site needs a new lexical shape. |
| Wrap multiple body forms without a thunk | A function would require #(do ...) or fn []. |
| Build a small DSL | The syntax must be constrained and expansion must stay reviewable. |
Macros are justified when the abstraction must decide whether body forms run.
1(defmacro when-enabled
2 [flag & body]
3 `(when ~flag
4 ~@body))
This is intentionally close to Clojure’s built-in when. The body forms are not evaluated before when-enabled expands; they are inserted into generated code.
1(when-enabled (:audit? config)
2 (record-audit! event)
3 :audited)
A function could not receive (record-audit! event) unevaluated unless the caller wrapped it in a function.
Macros can also introduce a clear scope around a body. The standard with-open form is the model: it creates a binding, runs a body, and guarantees cleanup.
1(defmacro with-tx
2 [tx & body]
3 `(let [tx# ~tx]
4 (try
5 (begin! tx#)
6 (let [result# (do ~@body)]
7 (commit! tx#)
8 result#)
9 (catch Throwable t#
10 (rollback! tx#)
11 (throw t#)))))
The expansion should still look like ordinary Clojure: let, try, do, and function calls. If the macro hides transaction semantics that the team cannot inspect, it is too large.
A macro-backed domain-specific language is reasonable only when the domain syntax removes real boilerplate and the generated code remains obvious.
| Good DSL sign | Bad DSL sign |
|---|---|
| Small vocabulary with predictable expansion | Many keywords with hidden side effects |
| Easy to macroexpand and review | Expansion is harder than the original problem |
| Domain experts can read call sites | Only the macro author understands the syntax |
| Plain data would be awkward or insufficient | A data map would have worked |
| Java habit | Clojure macro judgment |
|---|---|
| Annotation-generated boilerplate | Ask whether a macro can generate readable Clojure at the call site. |
| Reflection for dynamic behavior | Prefer runtime data/functions unless syntax really matters. |
| Template methods and inheritance hooks | Prefer higher-order functions first; use macros only for call-shape control. |
Before adding a macro to shared code, answer:
| Question | Required answer |
|---|---|
| What can a function not express here? | Syntax, binding, or evaluation control. |
| Is the expansion readable? | Yes, with macroexpand-1. |
| Are generated locals safe? | Yes, auto-gensyms or explicit gensyms. |
| Is the macro smaller than the problem it solves? | Yes, and examples fit on one screen. |