Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Proxy

A stand-in with the same interface — and the fact that callers cannot tell is both the feature and the hazard.


A gallery page shows a thousand images. Constructing a RealImage reads the file from disk. Constructing a thousand of them freezes the page for several seconds, and the user will scroll past ninety of them.

You want an object that behaves like an image but does not read the file until someone actually renders it.

The pattern

An object with the same interface as the real thing, holding a reference to it, deciding what to do before forwarding.

interface Image {
render(): void;
}
class RealImage implements Image {
constructor(private readonly path: string) {
this.pixels = readFileSync(path); // expensive, and happens right now
}
render() { /* … */ }
}
class LazyImage implements Image {
private real?: RealImage;
constructor(private readonly path: string) {} // cheap: stores a string
render() {
this.real ??= new RealImage(this.path); // pay on first use
this.real.render();
}
}

A thousand LazyImages cost a thousand strings. The disk is touched only for the ones that reach the screen.

Four reasons to do it

The structure is always the same; only the thing you do before forwarding changes.

What is the proxy for?

Virtual — defer the expensive part

class LazyImage implements Image {
  private real?: RealImage;

  render() {
    this.real ??= new RealImage(this.path);   // loaded on first use
    this.real.render();
  }
}

A thousand thumbnails construct instantly; only the ones scrolled into view ever touch the disk. In all three the interface is unchanged — that is what separates a proxy from an adapter.

The fourth, not shown above, is the remote proxy: an object that looks local and is actually a network call. Every RPC client stub is one.

Why the identical interface matters

It is what makes a proxy droppable. Nothing at the call site changes, no configuration flag is threaded through, and code written before the proxy existed picks it up.

JavaScript has one built in

Proxy intercepts operations on any object, including ones you did not write:

const audited = new Proxy(accounts, {
get(target, prop, receiver) {
log(`read ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
});

This is what powers Vue’s reactivity, most mocking libraries, and ORM lazy loading. It is genuinely powerful and worth knowing about — and it is also invisible to a reader of the call site, so it deserves the same caution as any other proxy, doubled.

Proxy or Decorator?

Structurally identical: same interface, holds one instance, forwards. The catalogue separates them by intent:

The practical difference is who decides. A decorator is composed at the call site by someone who wants the extra behaviour; a proxy is installed by infrastructure and the call site is not consulted.