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.
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.
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.
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.
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.
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.