Learn the macro use cases that matter in real Clojure code: controlling evaluation, introducing bindings, removing unavoidable boilerplate, and creating small readable DSLs without hiding behavior.
Macros are powerful when they express something a function cannot express. The most important use cases are syntax and evaluation control, not vague “power” or automatic performance.
For Java engineers, think of macros as a source-level abstraction tool. They can remove boilerplate, but the generated code must still be obvious enough to debug and review.
| Use case | Why a macro may fit |
|---|---|
| Control evaluation | A function cannot stop arguments from evaluating before the call. |
| Introduce bindings | The macro can create a local shape such as with-open, let, or testing forms. |
| Hide unavoidable boilerplate | Repeated try, resource, or instrumentation scaffolding can become a small syntax form. |
| Build a tiny DSL | Domain syntax can be clearer when the expansion remains ordinary Clojure. |
| Define declarations | Macros can generate def, defn, protocol, or registration code from one source form. |
If the problem is only “I want a shorter function call,” prefer a function.
A function cannot implement unless correctly because the body would evaluate before the function receives it. A macro can choose where the body appears in generated code.
1(defmacro unless
2 [condition & body]
3 `(when-not ~condition
4 ~@body))
5
6(unless (:active? user)
7 (println "User is inactive")
8 :skipped)
Expansion makes the behavior concrete:
1(macroexpand-1
2 '(unless (:active? user)
3 (println "User is inactive")
4 :skipped))
5
6;; roughly:
7(clojure.core/when-not (:active? user)
8 (println "User is inactive")
9 :skipped)
The macro is small because it creates a control-flow shape. That is a valid macro use case.
Macros often help when the caller needs a clean binding form. Clojure’s with-open is the standard example: it binds resources, executes the body, and closes resources reliably.
You can use the same idea for small application-specific boundaries:
1(def ^:dynamic *request-id* nil)
2
3(defmacro with-request-id
4 [request-id & body]
5 `(binding [*request-id* ~request-id]
6 ~@body))
This kind of macro is justified only if *request-id* is already an intentional dynamic Var boundary. Do not use macros to hide ordinary parameter passing just because Java method signatures used to be long.
Macros can reduce repeated scaffolding, but they should not hide important control flow.
1(defmacro with-timing
2 [label & body]
3 `(let [started# (System/nanoTime)]
4 (try
5 ~@body
6 (finally
7 (println ~label "took"
8 (/ (- (System/nanoTime) started#) 1000000.0)
9 "ms")))))
Notice the started# symbol. The trailing # asks syntax quote to generate a unique symbol, avoiding collisions with a caller’s local names.
| Java approach | Clojure macro question |
|---|---|
| Annotation plus processor | Is the generated Clojure code simpler to inspect with macroexpand? |
| Reflection utility | Is this really runtime discovery, or compile-time syntax generation? |
| Template method pattern | Would a higher-order function be clearer than a macro? |
| Lombok-style boilerplate removal | Does the macro hide behavior reviewers need to see? |
| Internal DSL with chained builders | Would data literals plus functions be enough? |
| Temptation | Better default |
|---|---|
| Speeding up slow code | Profile and improve data shape, algorithm, reflection hints, or interop. |
| Avoiding function names | Write smaller functions with better names. |
| Hiding complex behavior | Use explicit functions and data so callers can reason about it. |
| Runtime configurability | Use data, protocols, multimethods, or ordinary functions. |
| Copying Java annotations | Ask whether Clojure data-driven configuration is clearer. |