The problem
A photo gallery page shows 20 thumbnails, and clicking one opens the full-size picture: 12 MB of pixels. The natural model is a Photo interface with a display() method and a FullPhoto class whose constructor reads the image file. That class is simple and correct. Once a FullPhoto exists, it is ready to show.
The trouble is when the objects get created. If the page builds a FullPhoto for every thumbnail as it loads, it reads 20 × 12 MB = 240 MB before it can paint, although a typical visitor opens one photo and 228 MB of that is never shown. The obvious fixes spread the problem around. The page could create each photo on click, but then every piece of code that touches photos[i] has to check whether it exists yet. Or FullPhoto could load its pixels lazily, but then every method of the class checks its own state, and a class that described a photo now also decides when its data arrives.
The same tension appears whenever something has to happen around access to an object rather than inside it: checking that the viewer may see a private photo, calling an object that lives on another machine, remembering an answer that was expensive to compute. Put that logic in every caller and it gets copied and forgotten. Put it in the class and the class does two jobs.
How it works
Put a stand-in in front of the real object. The stand-in implements the same interface, so callers can't tell the two apart, and it decides when, whether and how each call reaches the real object. Gamma, Helm, Johnson and Vlissides catalogued Proxy in Design Patterns (1994) as a structural pattern, also known as Surrogate. Their motivating example is close to this one: a document editor keeps a stand-in object for each large embedded image and loads the image only when it has to be drawn. The stand-in already knows the file name and the image's size, which is enough to lay out the page.
| Participant | In the diagram | Role |
|---|---|---|
| Subject | Photo |
The interface the client uses. The real object and the proxy both implement it. |
| RealSubject | FullPhoto |
The object that does the work. Here it is expensive to create, because its constructor reads 12 MB. |
| Proxy | PhotoProxy, ProtectedPhoto |
Implements the Subject, holds the RealSubject or what it needs to create or find it, controls access and forwards calls. |
| Client | GalleryPage |
Works only with Photo and never learns which class it holds. |
In the diagram the page holds 20 PhotoProxy objects, each with a file name and an empty real field. The first display() on #7 creates the FullPhoto, which reads its file, and forwards the call. The second finds real set and forwards straight away. The page's code is the same as in step 1: only the line that fills photos changed, from new FullPhoto(n) to new PhotoProxy(n). In a larger application that line lives in a factory or a repository, and the page doesn't change at all.
Kinds of proxy
The structure stays the same; the reason for standing in between changes. The book describes four kinds, and caching and logging proxies are common additions.
- Virtual proxy. Creates an expensive object only when it is first needed, as
PhotoProxydoes. Lazy loading in an ORM is the everyday example. - Protection proxy. Checks whether the caller may perform an operation before forwarding it, as
ProtectedPhotodoes when bob opens ana's private photo in step 4. - Remote proxy. A local object that stands for one in another process or on another machine and turns each method call into a message. A gRPC client stub offers the same methods as the service and sends every call over the network. The book notes that Coplien called this kind of proxy an Ambassador.
- Smart reference. Does some bookkeeping on every access: counting references so the object can be freed when the last user lets go (C++'s
std::shared_ptrworks this way), loading a persistent object the first time it is touched, or checking that the object is locked before letting anyone change it. The book also shows a proxy hiding copy-on-write: callers share one copy of a large object, and the proxy copies it only when someone modifies it. - Caching proxy. Keeps the results of expensive or remote calls and answers repeats itself, so it has to decide how long an answer stays valid (see Cache-Aside for the invalidation side).
- Logging proxy. Records each call, its arguments and its result, without the caller or the real object knowing.
Proxy, Decorator and Adapter
All three wrap one object and forward calls to it. What they do to the interface, and who provides the object inside, tells them apart:
| Proxy | Decorator | Adapter | |
|---|---|---|---|
| Interface | the subject's own | the wrapped object's own | a different one: the interface the caller expects |
| The object inside | usually created or found by the proxy itself, or by the factory or framework that hands the proxy out | passed in by whoever composes the stack, often the client | passed in by the code that wires the adapter up |
| Intent | control access: when, whether and where calls reach the real object | add behaviour, with wrappers stacked in any order | make an existing class fit an interface it doesn't implement |
The book also distinguishes proxies by the reference they hold: a remote proxy has only an indirect one, such as a host and an address on that host, while a virtual proxy starts with an indirect one (the file name) and ends up holding the object itself. The edges still blur. A protection proxy that is handed its subject is built exactly like a decorator, and the book says as much; the difference is intent, because the proxy may refuse the call. A caching wrapper is a proxy when it guards an expensive or remote subject and a decorator when it is one optional layer among several. A Facade is different again: it puts a new, simpler interface over a whole subsystem instead of standing in for one object.
Code
TypeScript that mirrors the diagram. Node 22.18 or later runs it as is (node gallery.ts) because it strips the types, and the assertions check what the animation shows.
import assert from 'node:assert/strict';
// Subject: the only type GalleryPage knows.
interface Photo {
display(): string;
}
let loadedMB = 0; // full-size image data in memory: the gauge in the diagram
// RealSubject: the constructor reads the whole file, 12 MB per photo.
class FullPhoto implements Photo {
private readonly fileName: string;
constructor(fileName: string) {
this.fileName = fileName;
loadedMB += 12; // stands in for reading the 12 MB file
}
display(): string { return `showing ${this.fileName}`; }
}
// Virtual proxy: same interface; keeps the name, creates FullPhoto on first use.
class PhotoProxy implements Photo {
private readonly fileName: string;
private real?: FullPhoto;
constructor(fileName: string) { this.fileName = fileName; }
display(): string {
this.real ??= new FullPhoto(this.fileName); // only the first call reads the file
return this.real.display();
}
}
// Protection proxy: checks the signed-in user before forwarding.
class ProtectedPhoto implements Photo {
private readonly inner: Photo;
private readonly owner: string;
private readonly session: { user: string };
constructor(inner: Photo, owner: string, session: { user: string }) {
this.inner = inner;
this.owner = owner;
this.session = session;
}
display(): string {
if (this.session.user !== this.owner) throw new Error(`${this.session.user} may not see it`);
return this.inner.display();
}
}
const names = Array.from({ length: 20 }, (_, i) => `IMG_${String(i + 1).padStart(4, '0')}.jpg`);
const eager: Photo[] = names.map((name) => new FullPhoto(name));
assert.equal(loadedMB, 240); // step 1: 20 × 12 MB before the first paint
loadedMB = 0; // the same page again, with proxies
const photos: Photo[] = names.map((name) => new PhotoProxy(name));
assert.equal(loadedMB, 0); // step 2: 20 proxies, nothing read yet
console.log(photos[6].display(), `${loadedMB} MB`); // showing IMG_0007.jpg 12 MB
console.log(photos[6].display(), `${loadedMB} MB`); // showing IMG_0007.jpg 12 MB
assert.equal(loadedMB, 12); // step 3: two opens of #7, one 12 MB read
const session = { user: 'bob' };
const privatePhoto: Photo = new ProtectedPhoto(photos[6], 'ana', session);
assert.throws(() => privatePhoto.display(), /bob may not see it/); // step 4: refused
session.user = 'ana';
console.log(privatePhoto.display()); // showing IMG_0007.jpg
Output:
showing IMG_0007.jpg 12 MB
showing IMG_0007.jpg 12 MB
showing IMG_0007.jpg
GalleryPage sees only photos: Photo[]. Whether an entry is a FullPhoto, a PhotoProxy or a ProtectedPhoto in front of one is decided where the array is filled. The protection proxy here wraps the virtual proxy: proxies with different jobs can be combined, but the code that hands out the photos combines them, not the page that uses them.
When to use it
- An object is expensive to create or to keep in memory, and many are never used: images, documents, large records, connections. Create each one on first use behind a virtual proxy.
- Every caller must pass the same access check, and you would rather enforce it in one place than trust each caller to remember it.
- The real object lives in another process or service, and callers should use ordinary method calls. Generate the remote proxy from the service definition instead of writing it by hand.
- Repeated calls with the same arguments are expensive, and a slightly stale answer is acceptable.
- You can't change the real class (it belongs to a library) or its callers, but you do control where instances are created.
- Not when the object is cheap to create (a plain
newis clearer), when the client should choose and combine optional extras (use Decorator), when the interfaces differ (use Adapter), or when a single field should be lazy: a lazy property inside the class, such as??=on first use, Kotlin'sby lazyor .NET'sLazy<T>, does that with less ceremony.
Trade-offs
- The cost moves; it doesn't vanish. Start-up drops from 240 MB to 0 MB, but the first open of a photo now waits for its 12 MB. If that wait shows, prefetch the likely next one, such as the next photo in a slideshow.
- A cheap-looking call can be expensive, or fail in new ways.
display()may now read a file or cross the network, so it can be slow, throw an I/O error or time out. A remote proxy makes a network call look like a local one, which is exactly what makes it easy to call in a loop. - Identity changes. The proxy is a different object from its subject, so equality,
instanceofand map lookups see the proxy (see Implementation notes). - One more class to keep in step. Every method added to
Photohas to be added to each hand-written proxy, which is why wide interfaces are usually proxied by generated code: dynamic proxies, gRPC stubs, ORM proxies. - In return, the client and the real class stay unchanged, the access policy lives in one place, and expensive objects exist only once they are used.
Implementation notes
- Who creates the proxy. Not the client. A factory, a repository, a dependency-injection container or the framework hands out proxies, so the client keeps coding against the Subject. That is also how a proxy usually comes to manage its subject's lifetime, while a decorator's inner object comes from whoever builds the stack.
- Answer cheap questions without the real object. A proxy can keep metadata and serve it directly, as the book's image proxy reports the image's size for layout.
PhotoProxycould answer for the file name, the dimensions or the thumbnail and createFullPhotoonly fordisplay(). - Lazy creation and concurrency.
this.real ??= new FullPhoto(...)is safe in JavaScript only because it runs synchronously. Once the load is asynchronous, two clicks that arrive together can both findrealempty and start two 12 MB reads. Store the promise of the load instead of its result, so the second caller awaits the same one; it is the same race as the async trap in Singleton. In multithreaded runtimes two threads can race the same way. In Java, guard the check withsynchronizedor use double-checked locking on avolatilefield; .NET'sLazy<T>is thread-safe by default, and Kotlin'sby lazyis synchronized by default. The real object still has to be safe to share once it exists. - Identity and equality. A proxy is never
===its subject:photos[6] instanceof FullPhotois false, and aMapkeyed by the realFullPhotofinds nothing when it is given the proxy. Compare photos by an ID, not by reference. Generated proxies behave the same way:- MDN points out that a JavaScript
Proxyis a separate object with its own identity, and Vue documents that the proxy returned byreactive()is not equal to the original object. - A Java dynamic proxy sends
equals,hashCodeandtoStringto its invocation handler as well, so the handler decides what equality means. - Hibernate's guide warns that
equals()must cope with being passed a proxy, and thatinstanceofand casts don't work correctly on a proxy for a polymorphic association.
- MDN points out that a JavaScript
- Leaky behaviour. Lazy loading hides when the work happens, which makes it easy to trigger in a loop. List 20 photos and read
photo.album.titleon each, wherealbumis a lazy ORM proxy, and the page runs one query for the list and one more per photo, 21 in all. This is the N+1 selects problem, which Hibernate's guide names as the most frequent reason that data access code in Java runs slowly. Map associations as lazy, but fetch what a screen needs up front with a join fetch, or with batch or subselect fetching. Lifetimes leak too: a Hibernate proxy first touched after its session has closed throwsLazyInitializationException. - Functions need less. When the subject is a single function, a higher-order function is the whole proxy: a memoizing wrapper is a caching proxy, and a wrapper that checks a permission before calling through is a protection proxy. A dynamic
import()does for a module of code what a virtual proxy does for an object, loading it the first time it is needed. - Calls from inside skip the proxy. A proxy sees only the calls that go through it. In Spring AOP, when a method of the target calls another method on
this, the call bypasses the proxy, so the advice on the second method, a transaction for example, doesn't run. - Language and framework support.
- JavaScript:
Proxywraps any object in a handler whose traps (get,set,apply,hasand others) intercept its basic operations, usually forwarding them withReflect. A method that runs with the proxy asthiscan't reach the target's private#fieldsor a built-in's internal slots:new Proxy(new Map(), {}).sizethrows aTypeErrorunless the handler calls the method on the target itself. - Java:
java.lang.reflect.Proxy, available since Java 1.3, creates a class at runtime that implements a list of interfaces and sends every call to anInvocationHandler. It proxies interfaces, not classes. - Spring: Spring AOP uses those JDK dynamic proxies when the target implements at least one interface and generates a CGLIB subclass when it doesn't. Spring Boot may make class-based proxies the default, and since Spring Framework 7.0 the
@Proxyableannotation chooses the kind for an individual bean. - ORMs: Hibernate's
getReference()returns an unfetched proxy, and lazy associations are proxies that load on first use. EF Core's lazy-loading proxies (UseLazyLoadingProxies(), from theMicrosoft.EntityFrameworkCore.Proxiespackage) work on navigation properties that can be overridden, which meansvirtualones on classes that can be inherited from. - gRPC: the generated client stub is a remote proxy with the service's methods.
- Python:
weakref.proxyis a smart reference that doesn't keep its object alive; using it after the object has been collected raisesReferenceError.
- JavaScript:
- At architecture scale. The proxy moves out of the process and in front of a network endpoint. An Ambassador is a remote proxy that runs next to its client and adds retries, TLS and routing to the client's outbound calls. An API Gateway is a reverse proxy in front of many services that authenticates, rate-limits and routes, a protection proxy for a whole API. A CDN is a caching proxy for HTTP, and a Service Mesh puts a sidecar proxy next to every service. The idea carries over, since clients keep calling the same address with the same protocol, but the interface is now a protocol instead of a method signature, every call pays a network hop, and the proxy is a process to deploy, scale and monitor, configured rather than coded.