🧱 Structural

Proxy Pattern

Provides a surrogate or placeholder for another object to control access to it. A proxy can add functionality like lazy initialization, logging, access control, or caching without changing the real object's code, since both share the same interface.

Problem

You need to control access to an object—perhaps it's resource-intensive to create, lives on a remote server, or requires access-right checking. Putting this control logic directly into the object violates the single responsibility principle and may not always be possible (e.g., with third-party classes).

Solution

Create a proxy class with the same interface as the original service object. The proxy holds a reference to the real service and delegates requests to it, but adds pre-processing or post-processing. Common proxy types include virtual proxies (lazy loading), protection proxies (access control), remote proxies (network), and logging proxies.

Participants

RoleResponsibility
SubjectDeclares the common interface for RealSubject and Proxy
RealSubjectDefines the real object the proxy represents
ProxyMaintains a reference to the RealSubject and controls access to it; implements the same interface

TypeScript Example

TypeScript
interface ImageService {
  fetch(id: string): string;
}

class RealImageService implements ImageService {
  fetch(id: string) {
    console.log(`Fetching image ${id} from disk...`);
    return `<img data-id="${id}" />`;
  }
}

class CachedImageProxy implements ImageService {
  private cache = new Map<string, string>();
  constructor(private service: RealImageService) {}

  fetch(id: string) {
    if (!this.cache.has(id)) {
      this.cache.set(id, this.service.fetch(id));
    } else {
      console.log(`Cache hit for ${id}`);
    }
    return this.cache.get(id)!;
  }
}

class AccessControlProxy implements ImageService {
  constructor(
    private service: ImageService,
    private allowedUsers: Set<string>,
    private currentUser: string,
  ) {}

  fetch(id: string) {
    if (!this.allowedUsers.has(this.currentUser)) {
      throw new Error(`Access denied for ${this.currentUser}`);
    }
    return this.service.fetch(id);
  }
}

const real = new RealImageService();
const cached = new CachedImageProxy(real);
const secure = new AccessControlProxy(cached, new Set(["admin"]), "admin");

console.log(secure.fetch("img-001"));
console.log(secure.fetch("img-001")); // cache hit

Real-World Example

CDNs (Content Delivery Networks) are proxies that cache and serve content closer to users. ORM lazy loading uses proxies that fetch related data from the database only when accessed. JavaScript's built-in Proxy object enables custom property access behavior. In enterprise software, API gateways act as proxies for microservices, handling authentication, rate limiting, and load balancing.

When to Use

  • You need lazy initialization of a heavyweight object (virtual proxy)
  • You want to control access to an object based on permissions (protection proxy)
  • You need to log or audit requests to an object (logging proxy)
  • You want to cache results of expensive operations (caching proxy)
  • You need to reference an object in a different address space (remote proxy)

When Not to Use

  • The service object is lightweight and doesn't need access control or caching
  • The extra indirection level adds latency that isn't justified by the benefits
  • The proxy's interface becomes hard to maintain as the subject's interface grows

Understanding the Proxy Pattern

The Proxy pattern is one of the most important structural design patterns in object-oriented software design. Originally cataloged by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their seminal 1994 book "Design Patterns: Elements of Reusable Object-Oriented Software," this pattern continues to be a cornerstone of modern software architecture.

Provides a surrogate or placeholder for another object to control access to it. A proxy can add functionality like lazy initialization, logging, access control, or caching without changing the real object's code, since both share the same interface. The pattern involves the following key participants: Subject, RealSubject, Proxy. Each participant has a well-defined role, and the interactions between them create a flexible, maintainable structure that can evolve with changing requirements.

In TypeScript and modern JavaScript development, the Proxy pattern is particularly valuable because the language's type system and interface support allow you to express the pattern with compile-time safety. TypeScript generics can make Proxy implementations even more flexible and reusable while maintaining strong type checking throughout the pattern's structure.

Knowing when to apply the Proxy pattern is as important as knowing how. Common use cases include: You need lazy initialization of a heavyweight object (virtual proxy); You want to control access to an object based on permissions (protection proxy); You need to log or audit requests to an object (logging proxy). However, overusing design patterns can lead to unnecessarily complex code. Always evaluate whether the pattern genuinely solves your problem or if a simpler approach would suffice.

The Proxy pattern is related to several other design patterns: Adapter, Decorator, Facade. Understanding these relationships helps you choose the right pattern for your specific situation and combine patterns effectively when building complex systems.

Related Patterns

More Structural Patterns

Explore All Design Patterns

Browse our complete reference of 23 design patterns with TypeScript examples, UML participants, and practical guidance.