Free Interactive Course · Design Patterns

Proxy Design Pattern

A stand-in with the same interface as the real object — that decides whether the call happens at all.

Structural Patternsmediuma.k.a. Surrogatea.k.a. Stand-in
ShareXLinkedIn
In one sentence

Provide a surrogate or placeholder for another object to control access to it — deferring its creation, checking permissions, caching its answers, or sending the call somewhere else entirely.

01

The problem Proxy solves

The problem

Your admin tool lists two hundred customers. Each CustomerRecord is expensive: loading one pulls a large document from blob storage and makes a paid call to a credit bureau. The list only ever shows a name and a signup date.

So loading the page costs two hundred document reads and two hundred paid API calls, to render four hundred short strings. It takes eleven seconds and the finance team asks why the bureau invoice tripled.

The obvious fixes each make things worse somewhere else. Splitting CustomerRecord into CustomerSummary and CustomerDetail means every caller now has to know which one it holds. Adding a loadDetail() call that must be made before certain getters means every caller has to remember, and the ones that forget get a null. And none of this touches the second problem: support agents can see the credit report, and they should not be able to.

02

How the Proxy pattern works

Hand the caller something that looks exactly like the real object and quietly decides what happens on each call:

  1. The proxy implements the same interface as the real subject, so no caller can tell the difference and no call site changes.
  2. It holds a reference to the real subject — or, for a virtual proxy, just the information needed to create it later.
  3. On each call it decides: forward it, answer it itself, serve it from cache, or refuse it.
  4. The real subject is written as if none of this existed. It knows nothing about laziness, permissions or caching.
Four proxies, one shape. The GoF book names them and they're worth memorising, because interviewers ask and because they solve genuinely different problems: a virtual proxy defers an expensive object until first use; a protection proxy checks whether the caller is allowed; a caching proxy remembers answers; and a remote proxy makes a network call look like a local one — every gRPC or RMI client stub you have ever used is one. A smart reference (reference counting, logging access) is the fifth, quieter member of the family.
«interface» Customername() · creditReport() · exportPii()ProfilePageholds a CustomerCustomerProxylazy · permissions · cacheCustomerRecord40 MB · paid API callcallscreates onfirst needname() → answered by the proxy · creditReport() → loads, then cachessame interface as the real object — which is what makes it invisible, and dangerous
Participants. The Subject interface is implemented by both the Real Subject and the Proxy. The proxy holds a reference to the real subject — often creating it lazily — and controls every call on its way through. Because the client is typed against the interface, it cannot tell which one it holds, and that invisibility is both the pattern's strength and the source of every problem in the pitfalls section.
03

See it: the call that never arrives

Three calls, three different decisions. Ask for the name, then the credit report twice, then try to export the PII — watch how many of them actually reach the real record. Then remove the proxy and do exactly the same thing.

▶ Try it — load, cache, refuse

An interactive call tracer: ProfilePage calls name(), creditReport() and exportPii() on a CustomerProxy. The proxy answers name() from a cheap summary without loading anything, loads the real CustomerRecord on the first creditReport() and caches the second, and refuses exportPii() outright. Remove the proxy and every call loads the full record, while nothing checks permissions at all.

Two things are worth pausing on. With the proxy in place, rendering the list costs nothing — name() never loads the record. And exportPii() is refused before the object exists, so the denial costs nothing either. Turn the proxy off and that same call succeeds silently: no error, no audit line, just a CSV of personal data written by someone who wasn't allowed to ask.
04

Proxy pattern code examples

A virtual proxy that defers the expensive load, with a permission check on the way past.

public interface Customer {
    String name();
    CreditReport creditReport();
    void exportPii(Path destination);
}

public final class CustomerProxy implements Customer {

    private final CustomerId id;
    private final String cachedName;      // came free with the list query
    private final Permissions caller;
    private CustomerRecord real;          // created on first genuine need

    public CustomerProxy(CustomerId id, String cachedName, Permissions caller) {
        this.id = id;
        this.cachedName = cachedName;
        this.caller = caller;
    }

    @Override public String name() {
        return cachedName;                // the list never triggers a load
    }

    @Override public CreditReport creditReport() {
        return real().creditReport();
    }

    @Override public void exportPii(Path destination) {
        // A protection proxy refuses BEFORE the expensive object exists.
        if (!caller.has("pii:export")) {
            throw new AccessDenied("pii:export required");
        }
        real().exportPii(destination);
    }

    private synchronized CustomerRecord real() {
        // synchronized, not a plain null check: two threads hitting an unloaded
        // proxy is the classic way to pay the expensive cost twice.
        if (real == null) real = CustomerRecord.load(id);
        return real;
    }
}

// For a wide interface, don't hand-write the forwarding — generate it:
//
//     Customer guarded = (Customer) java.lang.reflect.Proxy.newProxyInstance(
//         Customer.class.getClassLoader(),
//         new Class<?>[]{ Customer.class },
//         (p, method, args) -> { check(method); return method.invoke(record, args); });
//
// This is exactly how Spring's @Transactional and Hibernate's lazy entities work:
// the bean you autowire is usually a generated proxy, not your class.
Read across the tabs: three languages hand you the pattern rather than making you write it — JavaScript's built-in Proxy, C#'s Lazy<T> and DispatchProxy, Java's java.lang.reflect.Proxy (the machinery behind Spring's @Transactional and Hibernate's lazy entities). Go pointedly does not, and writing the struct by hand is considered the feature, not the gap. And notice that every single sample deals with the same two hazards: thread-safe first load (synchronized, call_once, sync.Once, Lazy<T>, caching the promise) and refusing before loading — a permission check placed after the load is a check that already cost you the money.
05

How to implement Proxy

  1. Make sure the real subject is reachable through an interface. If callers name the concrete class, there is nothing you can substitute.
  2. Decide which kind you are building — virtual (defer), protection (check), caching (remember) or remote (send). Mixing all four into one class is how proxies become unreviewable.
  3. Implement the interface, holding either the subject or the identifier needed to create it.
  4. Do the permission check before creating the real subject, so a refused call costs nothing.
  5. Make lazy creation thread-safe with the tool your language gives you — Lazy<T>, sync.Once, std::call_once, a cached promise — not a bare null check.
  6. Keep the answers the proxy gives on its own (a cached name, a size) genuinely cheap and genuinely correct; a stale stub is worse than a slow load.
  7. For wide interfaces, generate the forwarding rather than hand-writing it, and log what the proxy refused — a silent denial is a support ticket you'll never solve.
06

When to use Proxy — and when not to

Use it when an object is expensive and often unused (virtual), when access needs checking and the subject shouldn't know about your permission model (protection), when repeated calls have the same answer (caching), or when the subject lives in another process or on another machine and you want that to look ordinary (remote).

Where it goes wrong

The N+1 query. The single most expensive consequence of this pattern in real systems. A lazy proxy makes each access look free, so someone writes a loop over two hundred orders that touches order.customer.name — and that is two hundred round trips, spread across a stack trace where none of them appear. ORMs give you this by default; measure the query count, don't reason about it.

Invisible latency. A remote proxy makes a network call look like a method call, which is exactly the point and exactly the danger. Local calls can't time out, retry, or fail halfway. Callers will treat it as free because it looks free.

Identity breaks. proxy == real is false, instanceof CustomerRecord is false, and reflection sees the proxy's class, not yours. Frameworks that generate proxies (Hibernate, EF Core, Spring) surprise people this way constantly.

Stale caches with no way out. A caching proxy with no invalidation is a bug with a delay on it. Decide the staleness budget when you write it, not when someone reports wrong data.

Silent refusals. A protection proxy that returns null or an empty list instead of raising is indistinguishable from missing data, and nobody will diagnose it. Fail loudly and log the denial.

You want to…UseBecause
Control whether and when a call reaches the objectProxySame interface, and the proxy decides.
Add behaviour around a call that always happensDecoratorA Decorator always delegates; a Proxy may refuse, defer or answer instead.
Make an incompatible class callableAdapterAdapter changes the interface; Proxy keeps it identical.
Offer a simpler way into a whole subsystemFacadeFacade fronts many objects with a new interface; Proxy stands in for one with the same interface.
Share one immutable object across thousands of usesFlyweightFlyweight reduces how many objects exist; Proxy controls access to the ones that do.
07

Quick check

🧠 Quick check
Your ORM returns entities with lazy-loading proxies. A report loops over 500 orders and prints order.customer.name for each. What happens?

In the wild

JavaHibernate lazy entity proxies, RMI stubs, and Spring AOP — the bean injected for a @Transactional class is a generated proxy that opens and commits the transaction around your method.
C#Lazy<T> as a virtual proxy, DispatchProxy for generated interception, and EF Core's lazy-loading proxies over virtual navigation properties.
JavaScriptThe built-in Proxy object — and Vue 3's reactivity, which wraps component state in one so reads can be tracked and writes can trigger re-renders.
Pythondjango.utils.functional.SimpleLazyObject — request.user is one, so the session and user query only run if a view actually uses it. weakref.proxy is the smart-reference flavour.
C++std::shared_ptr as a reference-counting smart reference, and std::vector<bool>::reference, the proxy object that makes vector<bool> behave unlike every other container.
GoGenerated gRPC client stubs — a remote proxy implementing the same interface as the server, turning each method call into a round trip.

Frequently asked questions

What is the difference between the Proxy and Decorator patterns?
Structurally almost nothing: both implement an interface and hold an instance of it. The difference is what the wrapper does with the call. A Decorator always passes the call along and adds something around it — logging, retries, buffering — and decorators are designed to stack. A Proxy controls the call: it may refuse it, delay it until the real object exists, answer it from a cache, or turn it into a network request. A rough test: if removing the wrapper changes what the object does, it's a decorator; if it changes when, whether or where the call happens, it's a proxy.
What are the four types of proxy?
Virtual — defers creating an expensive object until it's first genuinely needed (lazy loading). Protection — checks whether the caller is permitted before forwarding. Caching — remembers answers so repeat calls never reach the subject. Remote — represents an object in another process or machine, turning method calls into network calls; every gRPC or RMI client stub is one. GoF also lists smart reference, which does bookkeeping such as reference counting or access logging — std::shared_ptr is the everyday example.
Is lazy loading in Hibernate or EF Core the Proxy pattern?
Yes, and it's the most widely used implementation of it in the world. Both frameworks hand you a generated subclass of your entity whose association properties query the database on first access. It also explains two things that puzzle people: getClass() or GetType() returns a strange generated name rather than your class, and a loop that touches a lazy association fires one query per iteration — the N+1 problem, which is the pattern's invisibility working against you.
Does a Proxy have to have the same interface as the real object?
Yes — that's the defining constraint, and it's what separates Proxy from Adapter and Facade. The client is typed against the interface and must be unable to tell which implementation it holds, because substitutability is what lets you introduce caching or permission checks without editing a single call site. The moment you change the interface, callers must change too, and you've written an adapter instead.
Is JavaScript's Proxy object the Proxy pattern?
It's a general-purpose tool for building one, and more besides. The GoF pattern is a stand-in with the same interface that controls access; JavaScript's Proxy lets you intercept nearly any operation — property reads and writes, deletion, in checks, function calls — so it can implement the pattern in a few lines, and can also do things the pattern never described, such as Vue's dependency tracking. Use it for the pattern by all means, but remember the traps run on every access, and a proxy in a hot loop is measurably slower than the object it wraps.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.