Beginner12 min readDesign Patternslive prototype

Proxy

Put a stand-in object in front of the real one. The stand-in has the **same interface**, so the client can't tell the difference — but the stand-in decides *if* and *when* the real object is created or called. A photo gallery doesn't load every 5 MB image up front: an `ImageProxy` waits until you actually look at a photo, loads it once, and serves it from cache after that.

The idea

What it is

Proxy is a stand-in. It's an object that has the same interface as the real object and sits in front of it, controlling access. The client calls the proxy exactly as it would call the real thing — and can't tell the difference. Behind the scenes, the proxy decides whether, when, and how to involve the real object: create it late, refuse the call, answer from a cache, or forward it across a network.

Picture a photo gallery. Each RealImage is a 5 MB file, and RealImage.display() reads it from disk — slow and expensive. If opening the gallery meant loading every photo, you'd wait forever for pictures you may never look at. So we put an ImageProxy in front of each one. It implements the same Image interface, but holds only the filename. Nothing is loaded until you actually call display() for the first time. Then the proxy loads the real image once, remembers it, and every later display() is instant. The gallery code never changes — it just talks to Images.

The one sentence to remember

A Proxy is a stand-in object with the same interface as the real one, which controls access to it — delaying its creation, guarding it, caching its answers, or reaching it over a network — while the client stays unaware anything is in between.

Same interface is the whole trick

The proxy only works because it's a drop-in replacement. If ImageProxy needed different method names or extra setup, every client would have to know about it — and the illusion breaks. Because both implement Image, you can hand a client a proxy or the real thing and it behaves identically. This is [[program-to-interfaces]] doing real work.

Mechanics

How it works

The three participants

Every proxy setup has the same small cast. The client only ever sees the first one:

  • Subject (interface) — the common contract, e.g. Image with a display() method. Client code depends only on this.
  • RealSubject — the heavyweight or sensitive object doing the actual work, e.g. RealImage, which loads 5 MB from disk in its constructor.
  • Proxy — implements the same Subject interface and holds a reference to the RealSubject (often null at first). Every call arrives here, and the proxy decides if and when to create or invoke the real object.

That decision — if and when — is the pattern's power. The proxy can delay creating the real object until the first call actually needs it (lazy). It can refuse the call outright (denied). It can answer from memory without forwarding at all (cached). Or it can serialize the call and send it to another machine (remote). Same shape, four superpowers.

The four classic kinds

  • Virtual proxy — delays creating an expensive object until it's first needed. Our ImageProxy is one: no disk read until the first display().
  • Protection proxy — checks who is calling before letting the call through. Guests can't reach deleteImage(); admins can.
  • Remote proxy — a local stand-in for an object living on another machine; it hides the network plumbing (this is exactly what RPC/gRPC stubs are).
  • Caching / smart proxy — memoizes results so repeat calls skip the real object, or adds housekeeping like reference counting and logging around each call.
typescript
interface Image {                       // Subject — the shared contract
  display(): void;
}

class RealImage implements Image {      // RealSubject — expensive!
  constructor(private filename: string) {
    console.log(`loading 5 MB ${filename} from disk…`);  // slow
  }
  display() { console.log(`showing ${this.filename}`); }
}

class ImageProxy implements Image {     // Proxy — same interface
  private real: RealImage | null = null;   // nothing loaded yet

  constructor(private filename: string) {} // cheap: just a string

  display() {
    if (!this.real) {                      // first call only:
      this.real = new RealImage(this.filename);  // load once (virtual)
    }
    this.real.display();                   // cached ever after
  }
}

const photo: Image = new ImageProxy("beach.png"); // instant — no disk
photo.display();   // loads 5 MB, then shows       (first call)
photo.display();   // shows instantly — from cache  (every later call)

Proxy vs Decorator — same shape, different intent

A [[decorator]] also wraps an object behind the same interface, so the two look identical in a class diagram. The difference is why. A Decorator ADDS behavior the client asked for — scrollbars, compression, borders — and clients often stack decorators on purpose. A Proxy CONTROLS ACCESS — it can create the object late, deny the call, or answer from cache — and the client usually doesn't know (or care) it's there. Decorator changes what you get; Proxy changes whether and how you reach it.

You already use proxies every day

  • ORM lazy relations — Hibernate hands you a proxy for order.getCustomer(); the actual database query fires only when you first touch a field.
  • JavaScript's built-in `Proxy` — wraps any object and intercepts every property read/write, powering things like Vue's reactivity.
  • API gateways — clients call one endpoint that authenticates, rate-limits, and logs before forwarding to real backend services.
  • CDN edge caches — a CDN node is a caching proxy for the origin server: repeat requests are answered at the edge and never travel to the origin.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

A photo gallery with a proxy standing in front of the disk. In the 💤 Lazy + cache tab, click a thumbnail: the client's display() call pulses to the proxy. The first time, the proxy has nothing cached, so the call continues to the disk — a loading bar runs and loaded 5 MB appears. Click the same photo again and the proxy answers instantly with a ⚡ from cache flash — the pulse never reaches the disk. Watch the two chips drift apart: requests climbs with every click, disk loads climbs only once per photo. That gap is the pattern. Flip to 🔒 Protection: the real object is deleteImage(). As 🙂 Guest, the call dies at the proxy with a red ✗ — the real object is never touched. Switch to 🛡 Admin and the same call passes straight through. Same interface, different gatekeeping.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Pay the disk cost exactly once

Open the prototype on the 💤 Lazy + cache tab and click 🖼 photo-1. Watch the pulse travel client → proxy → disk, the loading bar run, and loaded 5 MB appear — both chips read 1. Now click 🖼 photo-1 again: a ⚡ from cache flash on the proxy, the image appears instantly, and the pulse never reaches the disk. Requests is 2, disk loads is still 1.

try 02

Widen the gap

Click 🖼 photo-2 (one more disk load), then keep re-clicking photos you've already opened. The requests chip climbs on every click; disk loads stays frozen at the number of distinct photos you've opened. That widening gap is the virtual proxy + cache saving real work. Press ↺ Reset and the proxy forgets everything — the next click hits the disk again.

try 03

Get denied, then let through

Switch to the 🔒 Protection tab. With 🙂 Guest selected, press ▶ Delete photo: the pulse stops at the proxy with a red ✗ denied — the real deleteImage() card is never touched and real object calls stays 0. Now pick 🛡 Admin and press ▶ Delete photo again: the call passes through, the photo greys out, and the counter ticks to 1. Same button, same interface — the proxy alone decided who gets through.

In practice

When to use it — and what trips people up

Reach for a Proxy when you need a gatekeeper

The pattern earns its keep whenever something should happen between the client and the real object — without either of them knowing:

  • Lazy-init a heavy object — images, database connections, big reports. Pay the construction cost only when (and if) it's actually used.
  • Access control — the real object shouldn't check roles itself; a protection proxy screens callers before the call ever arrives.
  • Talk to a remote object — hide serialization and networking behind a local stand-in with the same interface (RPC stubs, service clients).
  • Cache or log without touching the real class — memoize expensive answers, count references, or record every call, all from the wrapper. The real class stays clean (see [[single-responsibility]]).

When NOT to use it

A proxy is one more layer of indirection: one more class, one more hop, one more place a bug can hide. If the real object is cheap to create and safe for anyone to call, a proxy adds ceremony and nothing else — just use the object. Add the stand-in when there's a real cost or a real gate, not by default.

What it gives you

  • Clients don't change — the proxy shares the Subject interface, so it drops in wherever the real object was expected.
  • Expensive work is deferred (and maybe avoided entirely) — the real object is created only when first needed.
  • Cross-cutting concerns stay out of the real class — access checks, caching, logging, and network plumbing live in the proxy, keeping RealSubject focused.
  • The proxy manages the real object's lifecycle — it can create it late, reuse it, or release it, without the client ever knowing.

Common mistakes

  • One more layer of indirection — an extra class to write and an extra hop to reason about when debugging.
  • The first call through a virtual proxy can pause noticeably — the deferred cost doesn't vanish, it just moves to the worst possible moment if you're unlucky.
  • Cached answers can go stale — a caching proxy that never invalidates serves yesterday's data with full confidence.
  • Hidden behavior can surprise — because the client can't see the proxy, a denied call or a lazy load looks like the real object misbehaving.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

interface Image {                        // Subject: the shared contract
  display(): void;
}

class RealImage implements Image {       // RealSubject: expensive to build
  constructor(private filename: string) {
    // pretend this reads 5 MB from disk — slow!
    console.log("loading " + this.filename + " (5 MB) from disk…");
  }
  display(): void {
    console.log("showing " + this.filename);
  }
}

class ImageProxy implements Image {      // Proxy: same interface, cheap
  private real: RealImage | null = null; // no image loaded yet

  constructor(private filename: string) {}

  display(): void {
    if (this.real === null) {            // virtual proxy: create lazily,
      this.real = new RealImage(this.filename);  // on the FIRST display()
    }
    this.real.display();                 // cache: reuse ever after
  }
}

// The gallery only knows the Image interface — proxy is invisible.
const gallery: Image[] = [
  new ImageProxy("beach.png"),           // instant — nothing loaded
  new ImageProxy("city.png"),
];

gallery[0].display();  // loads 5 MB, then shows   (first call pays)
gallery[0].display();  // shows instantly           (cache — no disk)
// city.png was never displayed → never loaded at all.

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What is the core idea of the Proxy pattern?

question 02 / 05

In the photo gallery, when does a virtual proxy load the 5 MB RealImage from disk?

question 03 / 05

Proxy and Decorator have the same class-diagram shape. What actually separates them?

question 04 / 05

A guest calls deleteImage() through a protection proxy and is denied. What happened to the real object?

question 05 / 05

Which of these is a remote proxy?

0/5 answered