Hermes Wiki

Proxy Pattern

Concept

Proxy gives an object a stand-in that implements the same interface as the real thing, so every caller can keep calling through that interface without knowing whether they're talking to the real object or a surrogate controlling access to it. The proxy holds a reference to the real subject and decides, on each call, whether to forward the call as-is, do work before or after forwarding it, or refuse to forward it at all — none of which the caller can observe from the interface alone.

The GoF catalog names four common flavors, distinguished by what the "extra work" is:

  • Virtual proxy — defers creating an expensive real object until it's actually needed (lazy loading). A document viewer can show a placeholder for a large embedded image and only decode/load the real bitmap when it scrolls into view.
  • Protection proxy — checks access rights before allowing a call through, without the real object needing any awareness of who's calling. Used where the object doing the work shouldn't also own authorization logic.
  • Remote proxy — represents an object that lives in a different address space (another process or machine), hiding the network call behind a normal-looking local method call. gRPC- and RPC-generated client stubs are remote proxies: calling client.GetUser(id) looks like a plain method call but marshals arguments, sends a network request, and unmarshals the response.
  • Logging/caching proxy — wraps calls to record them or to short-circuit a repeated call with a cached result, without touching the real subject's code at all.

All four share the same structural shape: Proxy and RealSubject both implement Subject; callers depend only on Subject. This is also what makes Proxy easy to confuse with Decorator — structurally near-identical, same interface delegation — but the two exist for different reasons. Decorator's whole purpose is to add behavior/responsibility (and decorators are meant to be stacked); Proxy's purpose is to control access to a specific real object, and typically there's exactly one proxy in front of a given real subject rather than a stack of them.

Tradeoffs

Approach Benefit Cost
Proxy Access control, laziness, caching, or remoting added transparently — real subject's code is untouched, callers see no interface change Adds an indirection layer per call; a poorly-implemented proxy (e.g. a protection proxy with a subtle bypass) creates a false sense of security since callers can't see the check happened
Modify the real object directly (bake in the auth check / lazy-init / cache) No extra layer or interface Couples an orthogonal concern (access control, laziness) into the object's own logic — violates single responsibility, and the check/optimization can't be reused across other objects that need the same treatment
Middleware/interceptor at a framework layer (e.g. HTTP middleware, ORM query hooks) Centralizes the concern across many objects/endpoints at once, less boilerplate than one proxy per class Coarser-grained — hard to apply per-object policy; only works where the framework actually exposes an interception point, which plain in-process object calls often don't
Aspect-oriented / bytecode-weaving approach Even less boilerplate than hand-written proxies for cross-cutting concerns applied broadly Adds real complexity and "magic" — behavior is injected outside the normal call graph, which makes debugging and reasoning about control flow harder

The proxy's core cost is easy to underestimate: every call site pays the indirection whether or not the proxy actually does anything interesting for that particular call, and a caching or protection proxy in particular introduces state (a cache, a permission check) that must stay correct as the system evolves — a caching proxy with a stale invalidation rule is a bug that looks exactly like "the real object returned wrong data" from the caller's side.

When to use / when not to

  • Use a virtual proxy when constructing the real object is genuinely expensive (large file load, network round trip, heavy computation) and callers frequently don't end up needing it.
  • Use a protection proxy when the object doing the work shouldn't be responsible for deciding who's allowed to call it — e.g. a domain object that should stay ignorant of the current user/session.
  • Use a remote proxy whenever a client library needs to make a network call look like a local one — this is what most RPC/gRPC-generated clients already do for you; reach for hand-writing one only when building that kind of client library yourself.
  • Don't add a proxy purely to "future-proof" a class that's cheap to construct and has no access-control or remoting need today — it's pure indirection with no payoff, and every caller now pays for an interface hop that does nothing.
  • Don't use a caching proxy as a substitute for actually understanding the invalidation problem — a cache with no correct invalidation strategy just relocates staleness bugs behind an innocent-looking interface.

Common pitfall

Treating a protection proxy as a substitute for real authorization at the trust boundary, rather than as one extra layer of defense. If the real subject is reachable through any path that doesn't go through the proxy — a direct reference leaked to calling code, a different code path that constructs the real object itself — the protection check is bypassed entirely and silently, because nothing in the interface signals that a check was supposed to happen. This is the same failure shape as relying on client-side validation for security: the check exists, but only for callers polite enough to go through the intended door.

Engineering Lens

The design-review question worth asking about a proxy isn't "does it implement the interface correctly" — that's mechanical — it's "can the real subject be reached by any path that skips this proxy." A protection or logging proxy is only as strong as the guarantee that it's the sole entry point to the object it wraps; if the real object is exported, injected, or constructible independently anywhere else in the codebase, the proxy is decorative rather than enforcing anything. This is also the point where Proxy and dependency-injection design intersect in practice: a DI container that hands out the real subject directly to one consumer and the proxy to another has quietly broken the pattern's central assumption.

  • Decorator Pattern — structurally near-identical (both wrap an object behind the same interface), but Decorator adds behavior/responsibility and is meant to be stacked, while Proxy controls access to one real subject and typically stands alone
  • Facade Pattern — both add a layer in front of something, but Facade simplifies a complex subsystem's interface for convenience, while Proxy preserves the same interface as a single object and controls access to it

Sources

Hermes Wiki