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:
The proxy implements the same interface as the real subject, so no caller can tell the difference and no call site changes.
It holds a reference to the real subject — or, for a virtual proxy, just the information needed to create it later.
On each call it decides: forward it, answer it itself, serve it from cache, or refuse it.
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.
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.
class CustomerProxy:
"""__getattr__ is only called for attributes that AREN'T found normally, which
makes it the perfect hook for a virtual proxy: define the cheap things, and
let everything else trigger the load."""
def __init__(self, customer_id: str, cached_name: str, permissions: set[str]) -> None:
self._id = customer_id
self._name = cached_name
self._permissions = permissions
self._real: CustomerRecord | None = None
@property
def name(self) -> str:
return self._name # answered without loading anything
def export_pii(self, destination: Path) -> None:
if "pii:export" not in self._permissions:
raise AccessDenied("pii:export required")
self._load().export_pii(destination)
def _load(self) -> CustomerRecord:
if self._real is None:
self._real = CustomerRecord.load(self._id)
return self._real
def __getattr__(self, item: str):
# Anything not defined above — credit_report, addresses, … — loads first.
return getattr(self._load(), item)
# Django ships this pattern where you'd least expect it: `request.user` is a
# SimpleLazyObject, so the session lookup and user query only happen if a view
# actually touches the user. Views that don't, don't pay.
#
# For the single-attribute case, functools.cached_property is the whole pattern
# in one decorator — compute once on first access, keep the value thereafter.
#include <memory>
#include <optional>
class Customer {
public:
virtual ~Customer() = default;
virtual std::string name() const = 0;
virtual CreditReport creditReport() = 0;
};
class CustomerProxy final : public Customer {
public:
CustomerProxy(CustomerId id, std::string cachedName)
: id_(std::move(id)), name_(std::move(cachedName)) {}
std::string name() const override { return name_; }
CreditReport creditReport() override { return real().creditReport(); }
private:
CustomerRecord& real() {
// std::call_once, not `if (!ptr)`: this is the same double-checked
// locking hazard the Singleton page walks through, and the standard
// library already solved it.
std::call_once(flag_, [this] { real_ = CustomerRecord::load(id_); });
return *real_;
}
CustomerId id_;
std::string name_;
std::once_flag flag_;
std::unique_ptr<CustomerRecord> real_;
};
// C++ is soaked in proxies, and two are worth knowing by name:
//
// std::shared_ptr — a smart reference: a proxy that counts references and
// destroys the subject when the last one goes away.
// std::vector<bool>::reference — the notorious one. vector<bool> packs bits,
// so operator[] cannot return a real bool&; it returns a
// PROXY object that pretends to be one. That is why
// `auto b = v[0];` behaves unlike every other container.
public interface ICustomer
{
string Name { get; }
CreditReport CreditReport();
void ExportPii(string destination);
}
public sealed class CustomerProxy(CustomerId id, string cachedName, IPermissions caller) : ICustomer
{
// Lazy<T> is a virtual proxy with the thread safety already argued about
// and settled — it is thread-safe by default.
private readonly Lazy<CustomerRecord> _real = new(() => CustomerRecord.Load(id));
public string Name => cachedName; // no load
public CreditReport CreditReport() => _real.Value.CreditReport();
public void ExportPii(string destination)
{
if (!caller.Has("pii:export"))
throw new UnauthorizedAccessException("pii:export required");
_real.Value.ExportPii(destination);
}
}
// For a wide interface, DispatchProxy generates the forwarding at runtime:
//
// public class AuditProxy<T> : DispatchProxy where T : class
// {
// public T? Target { get; set; }
// protected override object? Invoke(MethodInfo? m, object?[]? args)
// {
// Log(m!.Name);
// return m.Invoke(Target, args);
// }
// }
//
// EF Core's lazy-loading proxies are the same machinery: mark a navigation
// property `virtual`, and the entity you get back is a generated subclass that
// queries the database the first time you touch it.
// JavaScript is the one language where this pattern is a built-in language
// feature, named after itself. A Proxy intercepts operations on any object.
const customerProxy = (id, cachedName, permissions) => {
let real = null
const load = () => (real ??= CustomerRecord.load(id))
return new Proxy(
{ name: cachedName },
{
get(stub, prop) {
if (prop in stub) return stub[prop] // answered without loading
if (prop === 'exportPii' && !permissions.has('pii:export')) {
return () => { throw new AccessDenied('pii:export required') }
}
const value = load()[prop]
return typeof value === 'function' ? value.bind(real) : value
},
},
)
}
// `.bind(real)` matters: pull a method off the subject and call it through the
// proxy and `this` would otherwise be the proxy, not the record.
//
// Vue 3's entire reactivity system is this — component state is wrapped in a
// Proxy whose `get` trap records which component read which property, so a later
// `set` knows exactly what to re-render.
package customers
// Go has no dynamic proxies: no reflection-based interception, no generated
// subclasses. You write the struct, and the compiler checks you implemented the
// whole interface — verbose, but there is no hidden magic to debug at 3am.
type Customer interface {
Name() string
CreditReport(ctx context.Context) (Report, error)
ExportPII(ctx context.Context, dst string) error
}
type proxy struct {
id string
name string
perms map[string]bool
once sync.Once
real *Record
terr error
}
func (p *proxy) Name() string { return p.name } // no load
func (p *proxy) load(ctx context.Context) (*Record, error) {
p.once.Do(func() { p.real, p.terr = Load(ctx, p.id) })
return p.real, p.terr
}
func (p *proxy) ExportPII(ctx context.Context, dst string) error {
if !p.perms["pii:export"] {
return ErrForbidden // refused before anything is loaded
}
rec, err := p.load(ctx)
if err != nil {
return err
}
return rec.ExportPII(ctx, dst)
}
// The remote proxy is everywhere in Go and nobody calls it one: a generated
// gRPC client stub implements the same interface as the server handler, and
// turns each method call into a network round trip.
interface Customer {
readonly name: string
creditReport(): Promise<CreditReport>
exportPii(destination: string): Promise<void>
}
class CustomerProxy implements Customer {
#real: CustomerRecord | null = null
constructor(
private readonly id: CustomerId,
readonly name: string, // cheap, came with the list query
private readonly permissions: ReadonlySet<string>,
) {}
async creditReport(): Promise<CreditReport> {
return (await this.#load()).creditReport()
}
async exportPii(destination: string): Promise<void> {
if (!this.permissions.has('pii:export')) {
throw new AccessDenied('pii:export required')
}
await (await this.#load()).exportPii(destination)
}
async #load(): Promise<CustomerRecord> {
// Cache the PROMISE, not the value: caching the value still lets two
// concurrent callers start two loads before either finishes.
this.#loading ??= CustomerRecord.load(this.id)
return (this.#real ??= await this.#loading)
}
#loading: Promise<CustomerRecord> | null = null
}
// The native Proxy is typed as `ProxyHandler<T>`, but note the honest limit:
// TypeScript cannot verify that your `get` trap returns the right type per
// property, so a hand-written class like this one is the type-safe option.
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
Make sure the real subject is reachable through an interface. If callers name the concrete class, there is nothing you can substitute.
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.
Implement the interface, holding either the subject or the identifier needed to create it.
Do the permission check before creating the real subject, so a refused call costs nothing.
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.
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.
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…
Use
Because
Control whether and when a call reaches the object
Flyweight 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?
This is the N+1 problem, and it's the price of the pattern's invisibility: order.customer is a proxy, so touching .name triggers a load, once per order, from inside a loop where no line looks like a database call. It is fast on the ten rows you tested with and unusable on the fifty thousand in production. The fix isn't to abandon proxies — it's to tell the query to fetch the association up front (JOIN FETCH, Include, select_related) and to assert on the query count in a test, because the code itself will never look wrong.
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.
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 Proxycontrols 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.