Proxy

Intermediate · 11 min read · ▶ live playground · ✦ checkpoint

A photo gallery creates twenty high-resolution images while the page opens, even though the visitor views only one. Startup is slow because construction performs expensive loading long before any client asks to display those pixels.

The Proxy pattern puts a stand-in in front of another object to control access to it. The proxy implements the same interface as the real object, so the client keeps one contract while the stand-in adds laziness, authorization, caching, logging, or a remote boundary around the forwarded call.

That control costs indirection. Work may happen later than the client expects, and following one call now requires looking through another object.

Before: construction performs work nobody requested

RealImage loads its file during construction. Building a gallery therefore loads every image, including the ones that remain off-screen:

class RealImage {
  private filename: string;
 
  public constructor(filename: string) {
    this.filename = filename;
    this.loadFromDisk();
  }
 
  private loadFromDisk(): void {
    console.log(`loading ${this.filename}`);
  }
 
  public display(): string {
    return `showing ${this.filename}`;
  }
}
 
const gallery = [
  new RealImage("mountain.jpg"),
  new RealImage("ocean.jpg"),
  new RealImage("forest.jpg"),
];
 
console.log(gallery[0].display());

Moving the load into every caller would avoid eager construction, but then clients must remember an object lifecycle: create a placeholder, check whether the file is loaded, load it once, and finally display it. Access policy has leaked out of the object boundary.

After: keep the contract, intercept the call

The shared contract is the subject. RealSubject performs the actual work, while Proxy holds or creates that object and decides when a request may reach it.

Three common proxy intents differ in what they control:

  • A virtual proxy delays expensive creation or loading until the first request.
  • A protection proxy checks a caller's permission before forwarding a request.
  • A remote proxy represents an object elsewhere and turns a local-looking call into remote communication.

class diagram

Client reaches RealSubject through the same Subject contract

Client reaches RealSubject through the same Subject contract
  1. 1Subject is the stable interface the client sees whether access is direct or controlled.
  2. 2Client depends on Subject, so it does not need a special code path for the stand-in.
  3. 3RealSubject implements the contract and performs the expensive operation.
  4. 4Proxy implements that same contract, making it substitutable at the client boundary.
  5. 5Proxy controls the RealSubject reference and delegates only after its access rule passes.

The two dashed triangle arrows are the key shape: both real object and proxy satisfy Subject. The ownership edge is asymmetric. Proxy knows the real object; the real object does not need to know that a proxy exists.

For a virtual proxy, the first call has one extra step that later calls skip:

sequence diagram

The first display call creates the real image lazily

The first display call creates the real image lazily
  1. 1Client calls the Subject operation; ImageProxy receives it before any real image exists.
  2. 2On the first call, the proxy constructs RealImage and then delegates display().
  3. 3The result returns through the proxy; later calls reuse the stored RealImage.

The code keeps the lifecycle check in one place:

interface ImageSubject {
  display(): string;
}
 
class RealImage implements ImageSubject {
  private filename: string;
 
  public constructor(filename: string) {
    this.filename = filename;
    console.log(`loading ${this.filename}`);
  }
 
  public display(): string {
    return `showing ${this.filename}`;
  }
}
 
class ImageProxy implements ImageSubject {
  private filename: string;
  private realImage: RealImage | null = null;
 
  public constructor(filename: string) {
    this.filename = filename;
  }
 
  public display(): string {
    if (this.realImage === null) {
      this.realImage = new RealImage(this.filename);
    }
 
    return this.realImage.display();
  }
}
 
const image: ImageSubject = new ImageProxy("mountain.jpg");
 
console.log("proxy created");
console.log(image.display());
console.log(image.display());

proxy created loading mountain.jpg showing mountain.jpg showing mountain.jpg

Creating ImageProxy prints no loading line. The first display() constructs RealImage; the second reuses the stored instance, so loading appears exactly once.

A protection proxy would put an authorization check before the same delegation. A remote proxy would serialize the request, send it elsewhere, and turn the response back into the subject's result. The mechanics change, but the client-facing contract stays aligned with the real subject.

Proxy beside its look-alikes

These structural patterns can all wrap or coordinate another object, so intent matters more than the presence of delegation:

  • Decorator adds behavior through the same interface, and clients often stack several decorators.
  • Proxy controls access through the same interface, usually with one stand-in guarding one real subject.
  • Adapter changes the interface by translating an incompatible object into a target contract.
  • Facade simplifies a subsystem by introducing a new, smaller interface over several collaborators.

A logging wrapper can be described as either Decorator or Proxy if all you inspect is the class diagram. Ask why it exists. If logging is one optional responsibility in a stack, Decorator is the stronger description. If the wrapper governs access to a remote service and records each crossing, Proxy captures the design pressure.

When NOT to use it

A proxy that performs no access check, lazy creation, caching, remote communication, or other control adds indirection without changing the boundary. Give the client the real object.

Deep proxy chains also hide where work happens. A request that crosses an authorization proxy, cache proxy, retry proxy, and remote proxy may be flexible, but debugging its latency and failure path requires navigating every layer. Combine policies where they change together or make the chain visible in construction and diagnostics.

Virtual and remote proxies can surprise clients with first-call latency or failures that a local method name does not suggest. The same interface should mean compatible behavior, not a promise that time and failure modes are identical. The pattern's cost includes lifecycle state, delegation tests, and a less direct stack trace.

Checkpoint

Answer all three to mark this lesson complete

Structure is covered. The last pattern family is about how objects communicate and share responsibility at runtime; next, Strategy makes one behavior interchangeable, followed by Observer for event-driven collaboration.

+50 XP on completion