Browse Learn Clojure Foundations as a Java Developer

Choose Good Use Cases for Clojure Macros

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.

Evaluation Control

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.

Binding and Resource Scopes

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.

Small DSLs

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 Translation

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.

Approval Checklist

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.

Knowledge Check

### Which case most strongly justifies a macro? - [x] A call site needs unevaluated body forms inserted into generated control flow. - [ ] A helper function would be three characters longer. - [ ] A namespace has many small functions. - [ ] A Java library is available. > **Explanation:** Macros are justified by syntax and evaluation control. Shorter code alone is not enough. ### Why is `with-tx` a plausible macro? - [x] It introduces a scoped body and controls where the body forms run. - [ ] It makes database commits faster automatically. - [ ] It removes the need for error handling. - [ ] It converts transactions into Java annotations. > **Explanation:** The macro wraps caller body forms in generated `try`, `let`, and cleanup logic. That call shape is awkward with a plain function unless callers pass a thunk. ### What should a team inspect before accepting a macro-backed DSL? - [x] Representative expansions and the generated code shape. - [ ] Only the number of DSL keywords. - [ ] Whether the macro name sounds domain-specific. - [ ] JVM heap settings. > **Explanation:** DSL syntax is only safe when the generated Clojure is small, readable, and predictable.
Revised on Saturday, May 23, 2026