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.
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.
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 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:
Otherwise, functions and closures are easier to understand.
These patterns overlap in appearance but differ in emphasis:
A proxy often decides whether or when the target is touched at all. That is the key distinction.
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:
If the proxy hides all of that, the abstraction becomes dangerous.
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:
The result is harder-to-read code with little benefit.
A proxy should simplify usage, not erase the reality of a network boundary.
If the stand-in does not control access, creation, or location, decorator may be the better pattern name.
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.