Proxy Pattern with Macros and Functions in Clojure

Learn how Clojure uses stand-ins for lazy loading, access control, and remote boundaries, and why most useful proxies are ordinary functions rather than macro-heavy metaprogramming.

Proxy pattern: A structural pattern that places a stand-in in front of another component in order to control access, creation, or interaction.

Proxy is about indirection with purpose. The proxy is not just “another wrapper.” It exists because the real target should not be touched directly every time. Common reasons include lazy initialization, permission checks, caching, remote calls, or guarded access to expensive resources.

In Clojure, Proxies Are Usually Functions First

Although Clojure has a proxy macro for Java interop, most application-level proxy behavior is simpler and more idiomatic with ordinary functions.

 1(defn lazy-proxy [create-target]
 2  (let [instance (delay (create-target))]
 3    (fn [& args]
 4      (apply @instance args))))
 5
 6(defn expensive-service []
 7  (println "Initializing expensive service")
 8  (fn [x] (* x 10)))
 9
10(def service (lazy-proxy expensive-service))

The first call initializes the real target. Later calls reuse it. That is proxy behavior without class-heavy machinery.

Access-Control Proxies Are Another Common Fit

1(defn wrap-admin-only [f]
2  (fn [request]
3    (if (= :admin (:role request))
4      (f request)
5      {:status 403
6       :body "Forbidden"})))

This is still a proxy. The caller interacts with the stand-in, and the stand-in decides whether the real operation may proceed.

Macros Are Rarely the Main Story

Macros can help generate proxy scaffolding when many similar wrappers must be defined, but they are not usually the best first tool. If a function can express the indirection clearly, it is almost always the stronger choice.

Use macros only when:

  • the proxy definition itself is repetitive enough to justify generation
  • evaluation structure truly needs to be customized
  • Java interop or DSL shape makes the macro the clearest option

Otherwise, functions and closures are easier to understand.

Proxy Is Different from Decorator

These patterns overlap in appearance but differ in emphasis:

  • proxy controls access, creation, or location of the real target
  • decorator adds cross-cutting behavior around an already accessible component

A proxy often decides whether or when the target is touched at all. That is the key distinction.

Remote Boundaries Deserve Honest Proxies

Remote client wrappers are a classic proxy case. But they should not pretend a network call is a normal local function with no trade-offs. Good remote proxies make it visible that:

  • latency exists
  • timeouts matter
  • failures can be partial
  • retries and idempotency affect correctness

If the proxy hides all of that, the abstraction becomes dangerous.

Decide Which Operational Concerns the Proxy Owns

Once a proxy crosses a process or service boundary, the next design question is ownership. Decide whether the proxy is responsible for timeouts, retries, metrics, circuit-breaking, authentication headers, or fallback behavior, and keep that answer consistent.

That usually means:

  • a thin client proxy should expose failure clearly and let callers choose recovery
  • an infrastructure wrapper may own metrics, tracing, and standardized timeout policy
  • retries should live only where idempotency and backoff rules are understood
  • fallback logic belongs close to the business meaning of degraded service, not hidden inside every wrapper

Common Failure Modes

Using Macros for a Simple Wrapper Problem

The result is harder-to-read code with little benefit.

Hiding Expensive Remote Work Behind a “Normal” Call

A proxy should simplify usage, not erase the reality of a network boundary.

Confusing Proxy with Generic Decoration

If the stand-in does not control access, creation, or location, decorator may be the better pattern name.

Practical Heuristics

Use a proxy when access to the real target should be delayed, guarded, cached, or mediated. In Clojure, start with a function or closure. Reach for macros only when repeated proxy definitions truly need code generation. The cleanest proxy is the one that preserves clarity about what the target really is and what the stand-in is protecting you from.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026