Browse Learn Clojure Foundations as a Java Developer

Choose Between Macros and Reflection

Learn when to choose a Clojure macro, a function, a protocol, Java reflection, or a type hint instead of treating metaprogramming as one broad tool.

The practical question is not “which metaprogramming feature is more powerful?” The practical question is “what information is available at the point where I need to make the decision?”

If the decision depends on source shape, a macro may fit. If it depends on runtime classes or configuration, reflection may fit. If it is ordinary runtime data, use a function.

Choice Matrix

Problem Prefer
Transform values already available at runtime. Function, because data transformation does not need syntax rewriting.
Add call-site syntax that controls evaluation. Macro, because it can receive unevaluated forms.
Dispatch behavior by runtime type or capability. Protocol, multimethod, or ordinary polymorphism, because the problem is runtime dispatch.
Load a class named in configuration. Java reflection or a framework that hides it, because the type is not known until runtime.
Remove accidental Clojure interop reflection. Type hints or clearer code, because the compiler needs target type information.

Good Macro Use Cases

Macros are strongest when a function would force awkward quoting, delayed evaluation, or repeated boilerplate at every call site.

1(defmacro with-timing [label & body]
2  `(let [started# (System/nanoTime)]
3     (try
4       ~@body
5       (finally
6         (println ~label "took"
7                  (/ (- (System/nanoTime) started#) 1000000.0)
8                  "ms")))))

The macro controls evaluation of body and wraps it in try/finally. A function cannot receive those body forms unevaluated without changing the call-site shape.

Use macros for patterns such as:

Macro pattern Why a macro helps
Resource, timing, or transaction wrappers. The body must remain normal code at the call site.
Test or route definitions. The call site declares code and metadata together.
Conditional binding forms. Some expressions should run only when earlier bindings succeed.
Small DSL syntax. The syntax removes repeated structural noise.

Good Reflection Use Cases

Reflection is strongest when the code cannot know the target type or member until runtime.

1Class<?> pluginType = Class.forName(pluginClassName);
2Object plugin = pluginType.getDeclaredConstructor().newInstance();

Use reflection for patterns such as:

Reflection pattern Why reflection helps
Plugin loading. Class names come from runtime configuration.
Annotation-driven frameworks. Frameworks inspect user code they did not author.
Serialization libraries. Fields, methods, and constructors vary by application type.
Test discovery. Test methods are found and invoked by convention or annotation.

Bad Fits

Temptation Better move
Use a macro to avoid writing a small function. Write the function unless the syntax is actually different.
Use reflection because Java did it in a framework. Use direct calls or dependency injection when application code already knows its own types.
Use a macro to hide a type hint. Put the hint at the boundary so the interop problem stays visible.
Use eval for runtime configuration. Parse data and dispatch explicitly instead of executing code.

Review Questions Before You Commit

  1. Does this code need unevaluated caller forms?
  2. Is the target class, method, or field unknown until runtime?
  3. Would a function, protocol, multimethod, or data map be simpler?
  4. Can a teammate understand the generated code or dynamic lookup failure?
  5. Have hot interop paths been checked with *warn-on-reflection*?

If the answer to the first two questions is “no,” you probably do not need a macro or Java reflection.

Knowledge Check

### Which tool should you usually choose for ordinary runtime data transformation? - [x] A function. - [ ] A macro. - [ ] Java reflection. - [ ] `eval`. > **Explanation:** If the inputs are ordinary runtime values, a function keeps the code simple, testable, and direct. ### When is a macro justified over a function? - [x] When the caller's unevaluated forms or special call-site syntax are central to the abstraction. - [ ] Whenever it saves one line of code. - [ ] Whenever Java reflection seems slow. - [ ] When the target class name comes from configuration. > **Explanation:** Macros are for syntax and evaluation control. They should not be used merely to avoid writing normal functions. ### What is the right first response to accidental Clojure interop reflection in a hot path? - [x] Add clearer type information and verify with reflection warnings. - [ ] Rewrite the call as a macro. - [ ] Use `eval` to build a faster call. - [ ] Ignore it because reflection is always free on the JVM. > **Explanation:** Accidental interop reflection usually means the compiler cannot resolve the Java target statically. Type hints or clearer code address that directly.
Revised on Saturday, May 23, 2026