Structural Pattern (GoF)

Proxy

Provides a substitute or placeholder for another object, controlling access to it — without changing the interface the client already knows.

Intent

Control access to an object by interposing a substitute (the proxy) that implements exactly the same interface as the real object. The client sees no difference — it programs against the contract, and the proxy decides when, how, and whether to delegate the call to the real object.

Cataloged by the GoF (1994) as a structural pattern, the Proxy is the natural choice whenever direct access to the real object needs to be mediated: the object is expensive to create, lives in another process or machine, requires authorization, or deserves to have its calls logged or cached.

Problem

Imagine a FullReport object that loads and processes hundreds of megabytes of data as soon as it's instantiated. Your application creates this object at startup, but the user rarely opens the full report — in most sessions, they only check the summary.

Direct access to the real object creates concrete problems:

  • Unnecessary cost: the object is created and all the processing runs even if the result is never used in the session.
  • No control point: there's nowhere to check permissions, log accesses, or return a cached result without modifying the original object or the client.
  • Coupling to the lifecycle: the client needs to know exactly when to create, destroy, and cache the real object.

The naive solution would be to add conditionals in the client, creating coupling and mixing responsibilities. The Proxy solves this transparently: it replaces the real object with an intermediary that the client uses in exactly the same way.

Proxy vs Decorator: the key difference

Both the Proxy and the Decorator wrap an object while keeping the same interface. The distinction lies in the intent:

  • The Decorator adds business responsibilities to the object — timestamp, operation logging, content encryption. The focus is enriching what the object does.
  • The Proxy controls access to the object — lazy loading, caching, permission checking, remote communication. The focus is when and whether the real object is accessed, not what it does.

In practice, the Proxy frequently manages the lifecycle of the real object (creates it on demand or keeps it cached), while the Decorator assumes the object already exists and just adds behavior around the calls.

Proxy vs Adapter

The Adapter changes the object's interface — it converts an incompatible contract into the one the client expects. The Proxy keeps exactly the same interface as the real object: the client doesn't know it's talking to an intermediary.

Solution

The Proxy organizes the code into three participants:

  1. Subject (interface): the contract shared by the real object and the proxy. Defines the operations the client can call. E.g.: Report interface with the method generate(): string.
  2. RealSubject: the real object that does the work. E.g.: FullReport — loads data, processes it, and returns the report. Can be expensive to create or require privileged access.
  3. Proxy: implements Subject and keeps a reference to the RealSubject (created on demand or injected). Intercepts client calls, runs its control logic (lazy init, permission check, cache, log) and, when appropriate, delegates to the RealSubject.

Main Proxy types

  • Virtual (lazy-loading): defers creating the real object until first use. Ideal for objects expensive to initialize.
  • Protection: checks permissions before delegating. A client with insufficient access is denied without ever reaching the real object.
  • Remote: represents an object that resides in another process or machine. The proxy handles serialization and network communication.
  • Cache/logging: stores the result of the first call and returns it on subsequent ones, or logs every access for auditing.

Structure

      «interface» Subject
      ┌──────────────────────┐
      │ + generate(): string │
      └──────────────────────┘
    ▲                              ▲
  implements                     implements
  FullReport                     ReportProxy
   (RealSubject)                   (Proxy)
  ┌─────────────────────────┐    ┌────────────────────────────────┐
  │ + generate(): string    │    │ - real: FullReport | null      │
  │   (expensive operation) │    │ - cache: string | null         │
  └─────────────────────────┘    │ + generate(): string           │
                                 │   1. returns cache, if present │
                                 │   2. creates FullReport (lazy) │
                                 │   3. delegates & stores result │
                                 └────────────────────────────────┘
                                                  │ delegates (on demand)
                                                  ▼
                                             FullReport


Flow — first call (object doesn't exist yet):

  client.generate()
      │
      ▼ (via the Subject interface)
  ReportProxy.generate()
      │ cache == null → creates FullReport
      │ delegates to real.generate()
      │ stores the result in cache
      ▼
  returns string to the client


Flow — subsequent call (cache populated):

  client.generate()
      │
      ▼
  ReportProxy.generate()
      │ cache != null → returns immediately
      ▼
  returns string (without creating or calling RealSubject)

Code examples

Example 1 — Virtual proxy with lazy loading and cache

The proxy creates the real object only on first access and caches the result, eliminating the cost of initialization and reprocessing. The client uses exactly the same interface as the real object.

// ── Subject — interface shared by real and proxy ──────────────
interface Report {
  generate(): string;
}

// ── RealSubject — expensive to create and run ─────────────────
class FullReport implements Report {
  constructor() {
    // Simulates costly initialization (DB reads, processing...)
    console.log("[FullReport] Initializing and loading data...");
  }

  generate(): string {
    console.log("[FullReport] Generating report...");
    return "=== FULL REPORT ===\nSales: $1,234,567\nCustomers: 4,892";
  }
}

// ── Virtual proxy with lazy loading and cache ─────────────────
class ReportProxy implements Report {
  private real: FullReport | null = null;
  private cache: string | null = null;

  generate(): string {
    if (this.cache !== null) {
      console.log("[Proxy] Returning cached result.");
      return this.cache;
    }
    // Creates the real object only when needed (lazy)
    if (this.real === null) {
      this.real = new FullReport();
    }
    this.cache = this.real.generate();
    return this.cache;
  }
}

// ── Client code — only knows the Report interface ─────────────
function displayReport(report: Report): void {
  console.log(report.generate());
}

const proxy = new ReportProxy();
// Nothing has been created yet — the real object doesn't exist.

console.log("--- First call ---");
displayReport(proxy);
// [FullReport] Initializing and loading data...
// [FullReport] Generating report...
// === FULL REPORT ===

console.log("--- Second call ---");
displayReport(proxy);
// [Proxy] Returning cached result.
// === FULL REPORT ===

Example 2 — Protection proxy with permission checking

The proxy intercepts the call and checks whether the user has the required permission before delegating to the real object. The client doesn't need to implement this logic — and the real object doesn't need to know an access control layer exists around it either.

// ── Subject ───────────────────────────────────────────────────
interface FileService {
  read(path: string): string;
  write(path: string, content: string): void;
}

// ── RealSubject ───────────────────────────────────────────────
class RealFileService implements FileService {
  read(path: string): string {
    return `[content of ${path}]`;
  }
  write(path: string, content: string): void {
    console.log(`[Disk] Writing to ${path}: ${content}`);
  }
}

// ── Supporting types ──────────────────────────────────────────
type Role = "read" | "write" | "admin";

interface User {
  name: string;
  roles: Role[];
}

// ── Protection proxy ──────────────────────────────────────────
class ProtectedFileService implements FileService {
  constructor(
    private readonly real: RealFileService,
    private readonly user: User
  ) {}

  private hasPermission(role: Role): boolean {
    return this.user.roles.includes(role) ||
           this.user.roles.includes("admin");
  }

  read(path: string): string {
    if (!this.hasPermission("read")) {
      throw new Error(`User "${this.user.name}" doesn't have read permission.`);
    }
    console.log(`[Proxy] Read access granted to ${this.user.name}.`);
    return this.real.read(path);
  }

  write(path: string, content: string): void {
    if (!this.hasPermission("write")) {
      throw new Error(`User "${this.user.name}" doesn't have write permission.`);
    }
    console.log(`[Proxy] Write access granted to ${this.user.name}.`);
    this.real.write(path, content);
  }
}

// ── Usage ───────────────────────────────────────────────────────
const real = new RealFileService();

const reader: User = { name: "Ana", roles: ["read"] };
const readProxy = new ProtectedFileService(real, reader);

console.log(readProxy.read("/data/sales.csv"));
// [Proxy] Read access granted to Ana.
// [content of /data/sales.csv]

try {
  readProxy.write("/data/sales.csv", "new data");
} catch (e) {
  console.error((e as Error).message);
  // User "Ana" doesn't have write permission.
}

const admin: User = { name: "Carlos", roles: ["admin"] };
const adminProxy = new ProtectedFileService(real, admin);
adminProxy.write("/data/sales.csv", "updated data");
// [Proxy] Write access granted to Carlos.
// [Disk] Writing to /data/sales.csv: updated data

When to use

  • Lazy initialization (virtual Proxy): when creating the real object is expensive (in time, memory, or I/O) and it might not be needed in the current session — defer creation until first access.
  • Access control (protection Proxy): when different clients should have different permissions over the same object. The proxy centralizes the check without polluting the real object or the client.
  • Caching results: when the result of an expensive operation can be reused in subsequent calls with the same parameters — the proxy stores and returns it without delegating again.
  • Logging and auditing: when you need to log every call to an object without modifying its class.
  • Remote proxy: when the real object lives in another process or server. The local proxy handles serialization and communication.

When to avoid

  • When latency is critically important: every call goes through an extra layer. In high-performance loops, the indirection can be noticeable.
  • When the real object's interface changes often: the proxy needs to keep up with every contract change — if the Subject is unstable, the maintenance cost can outweigh the benefit.
  • As a substitute for dependency injection: a protection Proxy that handles global authentication can be a sign that the authorization design needs to be revisited at the architectural level.

Pros and cons

Pros

  • Controls the lifecycle of the real object without the client knowing — lazy creation, caching, on-demand destruction.
  • Adds cross-cutting behavior (logging, security, caching) without modifying the real object or the client.
  • Open/Closed Principle: new kinds of control require only new proxies, without touching the RealSubject.
  • Makes testing easier: in test contexts, a proxy can simulate the real object's behavior without depending on external resources.

Cons

  • Adds an indirection layer that can make the execution flow less obvious when reading the code.
  • The proxy needs to implement the entire Subject interface — large interfaces generate a lot of delegation boilerplate.
  • The system's behavior changes silently depending on which Subject implementation was injected — can make debugging harder.

Common pitfalls

1. Confusing Proxy with Decorator

The most frequent pitfall is implementing a Proxy when the intent is Decorator — or vice versa. The decisive question: are you controlling access to the object (Proxy) or adding business behavior to it (Decorator)? A cache proxy returns the result without calling the real object the second time — that's access control. A logging decorator logs the call and always delegates — that's behavior enrichment. The structure is the same; the intent is different.

2. Shared state in the cache

Warning: a proxy with a cache needs a clear invalidation policy. If the real object's underlying state can change (e.g.: database data updated by another part of the system), the proxy's cache will become stale. Define explicitly: is the cache permanent for the session, does it have a TTL, or is it invalidated by an event?

3. Proxy is not domain authorization

A protection Proxy is suitable for simple, cross-cutting controls (e.g.: checking whether the logged-in user has the required role). Don't use Proxy for complex authorization business rules that depend on data state — in that case, the logic belongs to the domain or to a dedicated authorization service.

4. Forgetting to delegate every method

If the Subject has five methods and the Proxy only intercepts one, the other four still need to be implemented — usually delegating directly to the real object. It's easy to forget a method and introduce a silent bug where the call "disappears" without doing anything (or throws an unexpected runtime exception in dynamic languages).

Related patterns

The Proxy shares structure with other patterns and is frequently compared or combined with them:

The Decorator is the pattern with the greatest structural similarity: both wrap the real object implementing the same interface. The difference is in the intent — Proxy controls access, Decorator adds behavior. The Adapter also wraps the object, but changes its interface; the Proxy never changes the interface. The Composite composes multiple objects into a tree — the Proxy always represents exactly one real object. The Bridge separates two independent axes of variation into their own hierarchies — a prospective design decision. The Proxy doesn't separate axes: it represents exactly one real object, keeps the same interface, and controls access to it; Bridge and Proxy have fundamentally different intents and structures.