Proxy Pattern
What the Proxy Pattern actually is
A proxy is a stand-in object that implements the same interface as a real object and sits in front of it, intercepting every call. Because the proxy and the real subject share one interface, the client cannot tell them apart — it just calls executeQuery(...) and the proxy decides what to do before, instead of, or after forwarding the call to the real subject.
That single "same interface, intercept the call" mechanism powers four classic kinds of proxy, each adding a different responsibility:
- Virtual proxy — defers creating an expensive real subject until it is first needed (lazy initialization).
- Protection proxy — checks permissions before forwarding the call (access control).
- Remote proxy — represents an object living in another address space or machine, hiding the network plumbing.
- Caching / smart-reference proxy — adds bookkeeping such as memoizing results, reference counting, or logging.
The worked example below is a caching proxy: it memoizes query results so a repeated query never hits the database twice. Note this is deliberately not a virtual proxy — the real subject is created up front, and the proxy's job is to cache results, not to defer construction. Keeping the kind straight matters, because it is exactly what fixes the misleading "builds the real subject lazily on a miss" narrative: in the code below, the real subject already exists before the first call.
Java implementation (caching proxy)
The proxy holds an eagerly-constructed real subject and a cache. computeIfAbsent runs its lambda only on a cache miss; on a hit it returns the stored value and the lambda never executes. This is the single source of truth the trace below must match.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
interface DatabaseQuery {
String executeQuery(String query);
}
class RealDatabaseQuery implements DatabaseQuery {
@Override
public String executeQuery(String query) {
System.out.println("Executing database query: " + query);
return "Result of " + query;
}
}
class CachedDatabaseQuery implements DatabaseQuery {
// Real subject is created eagerly, at proxy construction — NOT lazily on a miss.
private final DatabaseQuery realQuery = new RealDatabaseQuery();
private final Map<String, String> cache = new ConcurrentHashMap<>();
@Override
public String executeQuery(String query) {
// Lambda runs ONLY on a miss; on a hit it is never invoked and prints nothing.
return cache.computeIfAbsent(query, q -> {
System.out.println("Cache miss for: " + q);
return realQuery.executeQuery(q);
});
}
}
public class Solution {
public static void main(String[] args) {
DatabaseQuery query = new CachedDatabaseQuery();
System.out.println(query.executeQuery("SELECT * FROM users")); // call 1: miss
System.out.println(query.executeQuery("SELECT * FROM users")); // call 2: hit
}
}Trace of the Java program
Read the trace against the code above. The real subject already exists before main ever calls executeQuery, so no construction happens "on a miss" — only the cache lookup, the lambda, and the database call do.
| Step | Call | cache state before | computeIfAbsent path | What prints |
|---|---|---|---|---|
| 1 | executeQuery("SELECT * FROM users") | { } (empty) | Miss — key absent, so the lambda runs: prints the miss line, then calls the already-constructed realQuery; result is stored. | Cache miss for: SELECT * FROM usersExecuting database query: SELECT * FROM usersResult of SELECT * FROM users |
| 2 | executeQuery("SELECT * FROM users") | { "SELECT * FROM users" → "Result of SELECT * FROM users" } | Hit — key present, so the lambda does not run; the stored value is returned. No "Cache miss" line, no database call. | Result of SELECT * FROM users |
Full console output, in order:
Cache miss for: SELECT * FROM users
Executing database query: SELECT * FROM users
Result of SELECT * FROM users
Result of SELECT * FROM usersNote what is absent on call 2: there is no "cache miss" line and no "Returning cached result" line, because computeIfAbsent prints nothing on a hit. The proxy's caching is observable only by the missing database call the second time, not by any extra log line.
Go implementation (caching proxy)
The Go version is structurally identical — same interface, eagerly-held real subject, cache map — but it logs differently: it prints an explicit line on a hit. So its trace is necessarily different from Java's, which is why each language gets its own trace rather than one shared one.
package main
import (
"fmt"
"sync"
)
type DatabaseQuery interface {
ExecuteQuery(query string) string
}
type RealDatabaseQuery struct{}
func (r *RealDatabaseQuery) ExecuteQuery(query string) string {
fmt.Println("Executing database query:", query)
return "Result of " + query
}
type CachedDatabaseQuery struct {
realQuery DatabaseQuery // created eagerly in the constructor below
cache map[string]string
mu sync.RWMutex // guards cache; safe for concurrent callers
}
func NewCachedDatabaseQuery() *CachedDatabaseQuery {
return &CachedDatabaseQuery{
realQuery: &RealDatabaseQuery{},
cache: make(map[string]string),
}
}
func (c *CachedDatabaseQuery) ExecuteQuery(query string) string {
c.mu.RLock()
if result, ok := c.cache[query]; ok {
c.mu.RUnlock()
fmt.Println("Returning cached result for:", query) // prints on a HIT
return result
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
// double-check after acquiring write lock
if result, ok := c.cache[query]; ok {
fmt.Println("Returning cached result for:", query)
return result
}
fmt.Println("Cache miss for:", query) // prints on a MISS
result := c.realQuery.ExecuteQuery(query)
c.cache[query] = result
return result
}
func main() {
var query DatabaseQuery = NewCachedDatabaseQuery()
fmt.Println(query.ExecuteQuery("SELECT * FROM users")) // call 1: miss
fmt.Println(query.ExecuteQuery("SELECT * FROM users")) // call 2: hit
}Trace of the Go program — note the explicit hit line that the Java version does not produce:
Cache miss for: SELECT * FROM users
Executing database query: SELECT * FROM users
Result of SELECT * FROM users
Returning cached result for: SELECT * FROM users
Result of SELECT * FROM usersThe lesson in the difference: the proxy's contract (cache, forward only on a miss) is identical across both languages, but the logging is an implementation choice. Do not paste one language's console output under the other's code — verify the trace against the exact branch each implementation takes.
When to use it — and when not to
Reach for a proxy when you need to control or augment access to an object without changing the object or its clients. Concretely:
- Lazy/virtual — the real subject is expensive to create and might not be used (large documents, heavy network handles).
- Protection — different callers should get different access; the proxy enforces permission checks.
- Remote — the real subject lives elsewhere and you want to hide marshalling/transport.
- Caching / logging / metering — you want to memoize results, count calls, or log access transparently (the example above).
When NOT to use it:
- The extra functionality changes the result the client sees (e.g. wrapping output, adding behavior to the returned value). That is a Decorator's job, not a Proxy's.
- You only need a simpler front door to a complex subsystem with a different, narrower interface — that is a Facade, which deliberately does not share the subject's interface.
- The indirection buys nothing: if access is cheap, unconditional, and local, a direct call is clearer than a proxy class.
- The proxy would silently diverge from the real subject's behavior — transparency bugs (different exceptions, different timing under load) are a real cost.
Proxy vs. Decorator — the classic confusion
Proxy and Decorator are structurally identical: both implement the subject's interface and hold a reference to a wrapped object that they forward to. The difference is intent, and that is the only reliable way to tell them apart:
| Proxy | Decorator | |
|---|---|---|
| Purpose | Control access to the subject (when/whether/who can reach it). | Add behavior to the subject (augment what it returns or does). |
| Who creates the subject | Usually the proxy manages the subject's lifecycle (may create it lazily, or own it). | The decorator receives an already-built component from the caller and wraps it. |
| Stacking | Typically one proxy; not designed to be layered for cumulative behavior. | Designed to be stacked — each layer adds a feature. |
| Result the client sees | Same result as the real subject (caching returns the same value; access control returns it or denies). | An enhanced result (compressed, encrypted, formatted, etc.). |
Rule of thumb: if the wrapper is about governing the call (lazy, secured, remote, cached, logged), it is a Proxy. If it is about enriching the call's outcome, it is a Decorator. Same skeleton, opposite reasons.
Trade-offs
| Pros | Cons |
|---|---|
| Controlled access: a single choke point for security, access checks, and lifecycle. | Added indirection: one more class and one more hop to reason about. |
| Transparency: clients keep using the same interface — no client changes. | Latency: remote and protection proxies add real overhead per call. |
| Performance via caching / lazy init: avoids redundant or premature expensive work. | Behavioral drift: a proxy that does not faithfully mirror the subject (timing, exceptions) causes surprising bugs. |
| Open/closed: add cross-cutting concerns (logging, metering) without touching the subject. | Confusion with Decorator: identical structure invites misuse when intent is unclear. |
In short: a proxy is the right tool when you must govern access to an object transparently. If you instead want to enhance what the object produces, you want a Decorator; if you want a simpler, different front door to a subsystem, you want a Facade.
Operability fingerprints (what proxies break in production)
- Concurrent-miss stampede. If two threads miss the same key at once, a naive proxy fires the expensive real call twice (or N times for N callers) — a cache stampede. The Java example avoids it because
ConcurrentHashMap.computeIfAbsentis atomic per key; the Go example avoids it with the lock + double-check. Under real load you want explicit single-flight (one in-flight computation per key, others wait) so a hot cold-key doesn't hammer the backend. - A cache proxy with no invalidation. The example memoizes forever — correct for immutable query results, a stale-data bug the moment the underlying row changes. A caching proxy needs a TTL, an explicit invalidate/evict hook, or write-through, or it will confidently serve dead data.
- Broken object identity. A proxy is a different object from its subject, so
proxy == realSubjectis false andequals/hashCodemay not delegate — code that keys a map on identity, or relies on reference equality, breaks silently behind a proxy. - Chatty remote proxies. A remote proxy makes a network round-trip look like a local method call, which invites an N+1 pattern (a loop that calls the proxy per item). Batch the calls at the boundary; the whole point of hiding the network must not hide its cost.
Recall question
A caching proxy and a decorator both wrap an object and implement the same interface. How do you decide which pattern you are looking at?
Answer: The difference is intent, not structure. A proxy controls access to the original object — when or whether a call reaches it (lazy initialization, protection, caching, remote forwarding). A decorator enhances the result or behavior of the object after the call (adding formatting, compression, extra responsibilities). If removing the wrapper would change whether the real object is used, it is a proxy; if removing it would change what the client receives, it is a decorator.
Source
Based on the Gang of Four Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, Vlissides), "Proxy" and "Decorator" chapters, and the distinction between Proxy, Decorator, and Facade as summarized by Refactoring.Guru. The Java caching example follows the GoF "Protection/Remote/Virtual/Caching Proxy" taxonomy.
🤖 Don't fully get this? Learn it with Claude
Stuck on Proxy Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Proxy Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Proxy Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Proxy Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Proxy Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Proxy Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.