Symptom

An expensive-to-construct object (network call, large decode, distant resource) is created eagerly because clients need a handle to it, even when access may never happen. Or: every client must remember to check authorization, cache freshness, or initialization state before using a resource — the checks are scattered, easy to skip, frequently inconsistent.

Goal

A Proxy that conforms to the real subject's interface and decides — based on the proxy's responsibility (lazy load, access control, caching, remote dispatch) — when and whether to forward to the real object. Clients call the same interface in either case; the policy lives in one place.

Before the pattern

class Image {
constructor(url) {
this.url = url;
this.pixels = downloadAndDecode(url);
}
draw(ctx, x, y) {
ctx.putImageData(this.pixels, x, y);
}
}
// Building a gallery thumbnail row:
const images = thumbnailUrls.map((url) => new Image(url));
// 100 thumbnails × 50MB decoded pixels = 5GB up front,
// even if the user only ever scrolls past 10 of them.

After the pattern

class Image {
constructor(url) {
this.url = url;
this.pixels = downloadAndDecode(url);
}
draw(ctx, x, y) {
ctx.putImageData(this.pixels, x, y);
}
}
class ImageProxy {
constructor(url) {
this.url = url;
this.real = null;
}
draw(ctx, x, y) {
if (this.real === null) {
this.real = new Image(this.url);
}
this.real.draw(ctx, x, y);
}
}
const images = thumbnailUrls.map((url) => new ImageProxy(url));
// ~24 bytes per proxy. Only the images that draw() is called on materialize.
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 4. The book's running example is a document with embedded images that load on demand; this JavaScript adaptation uses a thumbnail gallery because the lazy-loading payoff is concrete and the same-interface contract between Image and ImageProxy reads in code.
Pressure

Eager construction of expensive resources is a load-time tax paid for every potential use, even when most uses never happen. Per-client policy checks (auth, freshness, init-state) are Insider Trading on the resource's lifecycle — every caller is coupled to the resource's loading semantics.

Tradeoff

Two classes (Subject + Proxy) where one would have sufficed in some scenarios. Debugging requires the agent or human to determine which one their code is talking to; stack traces span both. The same-interface promise also blocks the proxy from exposing useful proxy-specific operations (preload, status, refresh) without leaking through the interface or adding a separate API.

Relief

Clients never decide when to load, when to check permissions, or when to invalidate — they just call the interface. The Subject stays focused on its core behaviour; the Proxy owns the access policy. Replacing the policy (eager preload, instant cache, remote dispatch) is a one-file edit at the Proxy.

Trap

Proxies that diverge silently from their Subject — different error modes, different return-type semantics, different concurrency semantics — break the same-interface contract clients depend on. The pattern's promise is interchangeability; violating it produces bugs at exactly the call sites the pattern was supposed to protect.