Use a practical decision framework for Clojure macros: start with functions and data, require a clear syntax or evaluation need, inspect expansion, and reject clever macros that make Java teams slower.
The default answer to “should this be a macro?” is usually “no.” That is not because macros are bad. It is because functions, data, protocols, multimethods, and higher-order functions solve most problems with less surprise.
A macro is worth it when it gives the caller a syntax shape that cannot be expressed cleanly with evaluated arguments.
| Question | If yes | If no |
|---|---|---|
| Does the caller need evaluation control? | A macro may be justified. | Prefer a function. |
| Does the caller need a new binding form? | A macro may be justified. | Prefer a function or data. |
| Is the expansion easy to read? | Keep going. | Simplify or reject the macro. |
| Are caller expressions evaluated once? | Keep going. | Fix multiple evaluation before use. |
| Can the team debug it at the REPL? | Keep going. | Do not hide the behavior behind syntax. |
Use this table before writing defmacro, not after the macro already exists.
Suppose you want validation. This does not need a macro:
1(defn validate-order
2 [{:keys [id total]}]
3 (cond-> []
4 (nil? id) (conj {:field :id
5 :message "Order id is required"})
6 (not (pos? total)) (conj {:field :total
7 :message "Total must be positive"})))
The function receives data and returns data. It is testable, composable, and obvious to a Java engineer reading it for the first time.
Use a macro only when the caller experience requires source-level shape:
1(with-open [reader (clojure.java.io/reader path)]
2 (doall (line-seq reader)))
with-open is macro-worthy because it creates a binding form and guarantees cleanup around a body. A function cannot receive that body before it evaluates.
Before naming the macro or documenting the API, expand representative calls.
1(macroexpand-1
2 '(with-open [reader (clojure.java.io/reader path)]
3 (doall (line-seq reader))))
The expansion may be longer than the call site. That is fine. Generated code can be verbose if it is predictable. The problem is not length; the problem is hidden behavior.
| Red flag | Why it is risky |
|---|---|
| The macro exists only to save a few parentheses. | It probably makes code less idiomatic, not clearer. |
| The expansion calls the same argument twice. | Caller expressions may repeat side effects or expensive work. |
| The macro creates invisible bindings. | Readers cannot tell where names come from. |
| Error messages point deep into generated code. | Debugging cost moves from the author to every caller. |
| A Java analogy is the only justification. | Clojure may have a simpler data/function shape. |
If a new macro would require a half-page explanation in a pull request, stop and try these first:
| Alternative | Use when |
|---|---|
| Function | You are transforming evaluated data. |
| Higher-order function | You need to pass behavior explicitly. |
| Data-driven configuration | The variation is declarative. |
| Protocol or multimethod | Behavior depends on type or dispatch value. |
| Existing macro | Clojure already has a standard binding or control-flow form. |
Macros are a team-level abstraction. Once introduced, everyone must learn the syntax, expansion, and failure modes.