Proxy

Provide a substitute for another object. A proxy intercepts access to the real object, letting you perform actions before or after the request — caching, access control, logging, lazy loading.

The Problem

You have a Database object. Some operations are expensive — loading a large dataset takes 3 seconds. You call it 10 times in a request loop, each time starting from scratch. Or you have a service that should only be accessible to admin users. Or you want to log every call without modifying the service class.

Adding caching/access-control/logging directly to the service violates the Single Responsibility Principle. And you can’t always modify the original class (third-party code).


The Solution

Create a Proxy that implements the same interface as the real object. The Proxy wraps the real object and intercepts calls, adding its extra behaviour before/after forwarding.

«interface» Subject + request(): void RealSubject + request() Proxy - realSubject: RealSubject + request() Proxy types: Virtual · Remote · Protection · Caching · Logging
Proxy — shares Subject interface with RealSubject; intercepts and controls access

Real-World Analogy

A credit card is a proxy for your bank account. You use the same interface (tap to pay) whether you’re using cash or a card. The card (proxy) performs extra work: authentication, fraud checks, logging — before money actually moves from your account. You don’t think about this; you just “pay.”


TypeScript Example

// Subject interface — both Proxy and RealSubject implement this
interface DataService {
  fetchUser(id: number): Promise<{ id: number; name: string }>;
  updateUser(id: number, name: string): Promise<void>;
}

// Real Subject — the actual implementation
class UserDataService implements DataService {
  async fetchUser(id: number): Promise<{ id: number; name: string }> {
    console.log(`[DB] Querying user ${id} (slow operation)`);
    await new Promise(r => setTimeout(r, 100)); // simulate latency
    return { id, name: `User ${id}` };
  }

  async updateUser(id: number, name: string): Promise<void> {
    console.log(`[DB] Updating user ${id} → "${name}"`);
    await new Promise(r => setTimeout(r, 80));
  }
}

// Caching + Logging Proxy
class DataServiceProxy implements DataService {
  private readonly cache = new Map<number, { id: number; name: string }>();
  private callLog: string[] = [];

  constructor(private readonly real: DataService) {}

  async fetchUser(id: number): Promise<{ id: number; name: string }> {
    this.callLog.push(`fetchUser(${id}) at ${new Date().toISOString()}`);

    if (this.cache.has(id)) {
      console.log(`[Cache] HIT for user ${id}`);
      return this.cache.get(id)!;
    }

    console.log(`[Cache] MISS for user ${id} — delegating`);
    const user = await this.real.fetchUser(id);
    this.cache.set(id, user);
    return user;
  }

  async updateUser(id: number, name: string): Promise<void> {
    this.callLog.push(`updateUser(${id}, "${name}") at ${new Date().toISOString()}`);
    this.cache.delete(id); // invalidate cache on write
    await this.real.updateUser(id, name);
  }

  getLog(): string[] { return [...this.callLog]; }
}

// Protection Proxy — wraps caching proxy with auth check
class AuthenticatedDataService implements DataService {
  constructor(
    private readonly inner: DataService,
    private readonly userRole: 'admin' | 'user' | 'guest'
  ) {}

  async fetchUser(id: number): Promise<{ id: number; name: string }> {
    if (this.userRole === 'guest') throw new Error('Unauthorised: guests cannot read user data');
    return this.inner.fetchUser(id);
  }

  async updateUser(id: number, name: string): Promise<void> {
    if (this.userRole !== 'admin') throw new Error('Unauthorised: only admins can update users');
    return this.inner.updateUser(id, name);
  }
}

// Wire it up — proxies compose cleanly
async function main() {
  const real    = new UserDataService();
  const cached  = new DataServiceProxy(real);
  const service = new AuthenticatedDataService(cached, 'admin');

  const u1 = await service.fetchUser(42);  // [Cache] MISS → [DB] query
  console.log(u1);                          // { id: 42, name: 'User 42' }

  const u2 = await service.fetchUser(42);  // [Cache] HIT — no DB call
  console.log(u2);                          // { id: 42, name: 'User 42' }

  await service.updateUser(42, 'Alice');    // invalidates cache
  const u3 = await service.fetchUser(42);  // [Cache] MISS again → DB
  console.log(u3);

  console.log(cached.getLog());
}

main();

When to Use

  • Virtual Proxy — delay expensive object creation until it’s actually needed
  • Caching Proxy — cache results of repeated expensive operations
  • Protection Proxy — gate access based on permissions
  • Logging Proxy — log, audit, or profile calls transparently
  • Remote Proxy — make a remote service feel like a local object (RPC, REST)

Pros

  • Adds behaviour without modifying the real subject
  • Proxies compose — stack a caching proxy inside an auth proxy inside a logging proxy
  • Can control object lifecycle (virtual proxy manages creation/destruction)
  • Open/Closed Principle — add new proxy types without changing the subject

Cons

  • Adds a layer of indirection — slightly more complex call stack
  • Response time increases (auth checks, cache lookups) — though often offset by what the proxy saves
  • If the proxy and real subject get out of sync (e.g., interface change), bugs can be subtle

  • Decorator adds behaviour and keeps the interface; Proxy also keeps the interface but controls access to the real object
  • Adapter changes the interface; Proxy keeps it
  • Facade simplifies a complex API; Proxy wraps a single object