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
| Role | Responsibility |
|---|---|
| Subject | Declares the common interface for RealSubject and Proxy |
| RealSubject | Defines the real object the proxy represents |
| Proxy | Maintains a reference to the RealSubject and controls access to it; implements the same interface |
TypeScript Example
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 hitReal-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
Converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn'…
Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing …
Provides a simplified interface to a library, framework, or complex subsystem. A facade doesn't encapsulate the subsyste…
More Structural Patterns
Explore All Design Patterns
Browse our complete reference of 23 design patterns with TypeScript examples, UML participants, and practical guidance.