Loading learning content...
In the previous page, we identified a fundamental design challenge: how do we control access to objects without modifying the objects themselves or burdening client code?
The Proxy pattern's solution is elegant in its simplicity: create a stand-in object that looks exactly like the real object but controls access to it.
Think of a proxy like a personal assistant to a busy executive. When you call the executive's office, you speak to the assistant who:
The caller might not even realize they're not speaking directly to the executive—the interface is identical.
By the end of this page, you will understand how the Proxy pattern is structured—the relationship between Subject interface, RealSubject, and Proxy. You'll see how interface conformance enables transparent substitution and learn the fundamental implementation patterns that all proxy types share.
The Proxy pattern defines three essential components that work together:
Subject (Interface) Defines the common interface for RealSubject and Proxy. This shared interface is what enables the Proxy to be used anywhere the RealSubject is expected.
RealSubject The actual object that does the real work. It implements the Subject interface and contains the business logic, expensive resources, or protected operations.
Proxy Maintains a reference to the RealSubject and implements the same Subject interface. The Proxy controls access to the RealSubject and may perform additional operations before or after forwarding requests.
The Subject interface is the linchpin of the pattern. By defining a contract that both RealSubject and Proxy implement, we gain the ability to substitute one for the other transparently. This is the Liskov Substitution Principle (LSP) in action.
Here's how the participants collaborate at runtime:
Let's build a concrete implementation step by step. We'll start with the foundational structure before exploring specific proxy types.
Step 1: Define the Subject Interface
The interface defines what operations are available. Both the real subject and proxy must implement this exactly.
12345678910111213141516171819202122232425262728293031323334353637383940
// Subject Interface - the contract both must fulfillinterface ImageLoader { /** * Load an image from the specified path * @returns The loaded image data ready for display */ loadImage(): ImageData; /** * Get the dimensions without fully loading the image * @returns Width and height in pixels */ getDimensions(): { width: number; height: number }; /** * Get metadata about the image (EXIF, format, etc.) */ getMetadata(): ImageMetadata; /** * Render the image to a canvas at specified dimensions */ render(targetWidth: number, targetHeight: number): Canvas;} // Type definitions for clarityinterface ImageData { pixels: Uint8Array; width: number; height: number; format: 'RGB' | 'RGBA' | 'Grayscale';} interface ImageMetadata { format: string; colorSpace: string; createdAt?: Date; camera?: string; location?: { lat: number; lng: number };}Step 2: Implement the RealSubject
The real subject contains the actual implementation. It knows nothing about proxies—it simply does its job.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// RealSubject - the actual implementationclass RealImageLoader implements ImageLoader { private imageData: ImageData | null = null; private metadata: ImageMetadata | null = null; constructor(private readonly imagePath: string) { // Note: We intentionally don't load in constructor // But a naive implementation might! } loadImage(): ImageData { if (!this.imageData) { console.log(`Loading image from disk: ${this.imagePath}`); // Expensive operation - reads file, decodes format const rawBytes = FileSystem.readFile(this.imagePath); this.imageData = ImageDecoder.decode(rawBytes); console.log(`Loaded ${this.imageData.pixels.length} bytes`); } return this.imageData; } getDimensions(): { width: number; height: number } { // For dimensions, we can read just the header // without loading the full image if (!this.metadata) { this.metadata = ImageDecoder.readMetadata(this.imagePath); } return { width: this.metadata.width, height: this.metadata.height, }; } getMetadata(): ImageMetadata { if (!this.metadata) { this.metadata = ImageDecoder.readMetadata(this.imagePath); } return this.metadata; } render(targetWidth: number, targetHeight: number): Canvas { const image = this.loadImage(); const canvas = new Canvas(targetWidth, targetHeight); canvas.drawImage(image, 0, 0, targetWidth, targetHeight); return canvas; }}Step 3: Create the Proxy
The proxy implements the same interface but adds control logic. Here's a basic proxy structure:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Proxy - controls access to RealSubjectclass ImageLoaderProxy implements ImageLoader { // Reference to the real subject - may be null initially private realLoader: RealImageLoader | null = null; // Cached lightweight data private cachedMetadata: ImageMetadata | null = null; constructor(private readonly imagePath: string) { // Proxy is cheap to create - doesn't load anything console.log(`Proxy created for: ${imagePath}`); } // Lazy initialization - create real subject only when needed private getRealLoader(): RealImageLoader { if (!this.realLoader) { console.log('Proxy: Creating real loader on first access'); this.realLoader = new RealImageLoader(this.imagePath); } return this.realLoader; } loadImage(): ImageData { console.log('Proxy: loadImage() called'); // Delegate to real subject return this.getRealLoader().loadImage(); } getDimensions(): { width: number; height: number } { console.log('Proxy: getDimensions() called'); // We can get dimensions from metadata - might not need real loader const metadata = this.getMetadata(); return { width: metadata.width, height: metadata.height }; } getMetadata(): ImageMetadata { console.log('Proxy: getMetadata() called'); // Cache metadata - it's lightweight if (!this.cachedMetadata) { // Read just metadata, doesn't need full image load this.cachedMetadata = ImageDecoder.readMetadata(this.imagePath); } return this.cachedMetadata; } render(targetWidth: number, targetHeight: number): Canvas { console.log(`Proxy: render(${targetWidth}, ${targetHeight}) called`); // Must delegate to real subject for full rendering return this.getRealLoader().render(targetWidth, targetHeight); }}Notice how the proxy adds value: it's cheap to create, caches lightweight metadata, and only creates the expensive RealImageLoader when truly necessary. The client sees the same interface but gets optimized behavior.
The beauty of the Proxy pattern lies in client code transparency. Clients work with the Subject interface and don't know—or care—whether they're using a proxy or the real object.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Client code - works identically with Proxy or RealSubjectclass ImageGallery { private images: ImageLoader[] = []; addImage(loader: ImageLoader): void { // Gallery doesn't know if this is a Proxy or RealSubject this.images.push(loader); } renderThumbnails(container: HTMLElement): void { for (const image of this.images) { // Only getDimensions is called for layout // If using proxies, heavy image data isn't loaded yet! const dims = image.getDimensions(); const placeholder = document.createElement('div'); placeholder.style.width = `${dims.width / 10}px`; placeholder.style.height = `${dims.height / 10}px`; placeholder.onclick = () => this.showFullSize(image); container.appendChild(placeholder); } } showFullSize(image: ImageLoader): void { // NOW the full image is needed // If proxy, this triggers actual loading const canvas = image.render(800, 600); this.displayCanvas(canvas); }} // Usage - client doesn't know about proxiesconst gallery = new ImageGallery(); // Could pass RealImageLoader or ImageLoaderProxy - same interface!gallery.addImage(new ImageLoaderProxy('/photos/vacation1.jpg'));gallery.addImage(new ImageLoaderProxy('/photos/vacation2.jpg'));gallery.addImage(new ImageLoaderProxy('/photos/vacation3.jpg'));// ... add 1000 more images // Renders thumbnails instantly - no heavy loading yetgallery.renderThumbnails(document.getElementById('gallery')!); // Only when user clicks does the specific image loadThe client code (ImageGallery) works identically whether you pass proxies or real subjects. This is true polymorphism—behavior varies based on the actual object type, but the interface remains constant.
A critical design decision in proxy implementation is how the proxy obtains and manages its reference to the real subject. There are several strategies, each with tradeoffs.
The proxy creates the real subject on first access. This is the most common approach for virtual proxies.
123456789101112131415161718
class LazyProxy implements Subject { private realSubject: RealSubject | null = null; private ensureRealSubject(): RealSubject { if (!this.realSubject) { // Created on first need this.realSubject = new RealSubject(this.config); } return this.realSubject; } request(): Result { return this.ensureRealSubject().request(); }} // Pros: Deferred creation, simple interface// Cons: Creation parameters must be stored by proxyThe real subject is provided to the proxy at construction. This is common for protection and logging proxies.
1234567891011121314151617181920
class InjectedProxy implements Subject { constructor(private readonly realSubject: RealSubject) { // Real subject already exists } request(): Result { // Pre-processing (logging, auth check, etc.) this.logAccess(); const result = this.realSubject.request(); // Post-processing this.logResult(result); return result; }} // Pros: Clear dependency, easy to test with mocks// Cons: Real subject must exist before proxy (no lazy creation)The proxy receives a factory that creates the real subject. This provides maximum flexibility.
12345678910111213141516171819202122232425262728
interface SubjectFactory { create(): RealSubject;} class FactoryProxy implements Subject { private realSubject: RealSubject | null = null; constructor(private readonly factory: SubjectFactory) {} private ensureRealSubject(): RealSubject { if (!this.realSubject) { this.realSubject = this.factory.create(); } return this.realSubject; } request(): Result { return this.ensureRealSubject().request(); }} // Usageconst proxy = new FactoryProxy({ create: () => new RealSubject(getConfig(), getDependencies())}); // Pros: Lazy + complex construction, easily testable// Cons: More indirection, factory interface needed| Strategy | Best For | Tradeoffs |
|---|---|---|
| Lazy Creation | Virtual proxies, expensive resources | Must store creation parameters; complex construction challenging |
| Injected Reference | Protection proxies, logging proxies | No lazy creation; real subject must exist first |
| Factory-Based | Complex dependencies, testable code | More indirection; requires factory interface |
| Service Locator | Remote proxies, dynamic discovery | Hidden dependencies; harder to test |
When the real subject has many methods, implementing proxy methods one by one becomes tedious. There are several strategies to handle this:
12345678910111213141516171819202122232425262728293031323334
// Explicit delegation - each method individually implementedclass ExplicitProxy implements ComplexService { constructor(private readonly real: RealComplexService) {} // Access control on specific methods sensitiveOperation(): void { if (!this.hasPermission()) { throw new UnauthorizedError('No permission'); } this.real.sensitiveOperation(); } // Caching on expensive methods expensiveQuery(criteria: Criteria): Results { const cacheKey = this.computeCacheKey(criteria); const cached = this.cache.get(cacheKey); if (cached) return cached; const result = this.real.expensiveQuery(criteria); this.cache.set(cacheKey, result); return result; } // Simple passthrough for other methods simpleGetter(): string { return this.real.simpleGetter(); } anotherGetter(): number { return this.real.anotherGetter(); } // ... potentially many more passthroughs}Modern languages offer mechanisms for dynamic proxies that intercept all method calls. This reduces boilerplate but may sacrifice type safety.
12345678910111213141516171819202122232425262728293031323334353637383940414243
// Dynamic proxy in JavaScript/TypeScript using Proxy objectfunction createLoggingProxy<T extends object>(target: T, logger: Logger): T { return new Proxy(target, { get(obj, prop) { const value = Reflect.get(obj, prop); if (typeof value === 'function') { return function(...args: unknown[]) { logger.log(`Calling ${String(prop)} with args:`, args); const start = performance.now(); try { const result = value.apply(obj, args); // Handle promises if (result instanceof Promise) { return result.then(r => { logger.log(`${String(prop)} resolved in ${performance.now() - start}ms`); return r; }); } logger.log(`${String(prop)} completed in ${performance.now() - start}ms`); return result; } catch (error) { logger.error(`${String(prop)} threw error:`, error); throw error; } }; } return value; } });} // Usage - no manual method implementations needed!const service = new RealComplexService();const loggedService = createLoggingProxy(service, console); // All method calls are now logged automaticallyloggedService.anyMethod(); // Logged!loggedService.anotherMethod(); // Logged!Dynamic proxies are powerful but have drawbacks: loss of static type checking, potential performance overhead, debugging can be confusing (stack traces include proxy machinery), and they may not work with all edge cases (constructors, getters, symbols). Use explicit delegation when behavior differs per method.
For languages supporting mixins, proxy behavior can be composed from reusable traits:
12345678910111213141516171819202122232425262728293031
// Reusable proxy behaviors as mixinsconst LoggingMixin = <T extends Constructor<{}>>(Base: T) => class extends Base { protected logMethodCall(method: string, args: unknown[]): void { console.log(`[${new Date().toISOString()}] ${method}`, args); } }; const CachingMixin = <T extends Constructor<{}>>(Base: T) => class extends Base { protected cache = new Map<string, unknown>(); protected withCache<R>(key: string, compute: () => R): R { if (this.cache.has(key)) { return this.cache.get(key) as R; } const result = compute(); this.cache.set(key, result); return result; } }; // Compose behaviorsclass EnhancedProxy extends LoggingMixin(CachingMixin(BaseProxy)) { expensiveOperation(input: string): Result { this.logMethodCall('expensiveOperation', [input]); return this.withCache(`expensive:${input}`, () => this.realSubject.expensiveOperation(input) ); }}Proxy and Decorator patterns look structurally similar—both wrap an object implementing the same interface. However, their intent differs fundamentally:
| Aspect | Proxy | Decorator |
|---|---|---|
| Primary Intent | Control access to object | Add responsibilities to object |
| Relationship | Proxy manages the wrapped object's lifecycle | Decorator enhances without managing lifecycle |
| Transparency | Client often doesn't know proxy exists | Decorators can be chained visibly |
| Creation | Proxy often creates the wrapped object | Decorator typically receives existing object |
| Multiplicity | Usually single proxy per subject | Multiple decorators commonly stacked |
| Focus | Access control, lazy loading, remote access | Adding features, modifying behavior |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// PROXY: Controls access - creates/manages the subjectclass ProtectionProxy implements Document { private realDocument: RealDocument | null = null; constructor( private documentId: string, private user: User ) { // Proxy creates subject internally } read(): string { // Access control - the core proxy purpose if (!this.user.canRead(this.documentId)) { throw new UnauthorizedError(); } return this.getDocument().read(); } private getDocument(): RealDocument { if (!this.realDocument) { this.realDocument = new RealDocument(this.documentId); } return this.realDocument; }} // DECORATOR: Adds behavior - wraps existing subjectclass EncryptionDecorator implements Document { constructor( private document: Document, // Receives existing subject private encryptionKey: string ) {} read(): string { // Adds behavior: decryption const encrypted = this.document.read(); return this.decrypt(encrypted); } write(content: string): void { // Adds behavior: encryption const encrypted = this.encrypt(content); this.document.write(encrypted); }} // Decorators stack; proxies typically don'tconst doc = new RealDocument('doc-123');const encrypted = new EncryptionDecorator(doc, 'key');const compressed = new CompressionDecorator(encrypted); // Stack decoratorsconst versioned = new VersioningDecorator(compressed);Think of Proxy as a 'gatekeeper' that decides IF and WHEN you can access something. Think of Decorator as a 'wrapper' that modifies WHAT you get when you access it. Proxy controls access; Decorator transforms results.
When proxies manage lazy initialization, thread safety becomes critical. Multiple threads may attempt to access the proxy simultaneously, potentially creating multiple instances of the real subject or causing race conditions.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// PROBLEM: Race condition in lazy initializationclass UnsafeProxy implements Subject { private realSubject: RealSubject | null = null; request(): Result { // Thread A: checks null -> true // Thread B: checks null -> true (before A creates) if (!this.realSubject) { // Both threads create instances! this.realSubject = new RealSubject(); } return this.realSubject.request(); }} // SOLUTION 1: Lock-based synchronizationclass SynchronizedProxy implements Subject { private realSubject: RealSubject | null = null; private mutex = new Mutex(); async request(): Promise<Result> { await this.mutex.acquire(); try { if (!this.realSubject) { this.realSubject = new RealSubject(); } return this.realSubject.request(); } finally { this.mutex.release(); } }} // SOLUTION 2: Double-checked lockingclass DoubleCheckedProxy implements Subject { private realSubject: RealSubject | null = null; private mutex = new Mutex(); async request(): Promise<Result> { // First check (no lock) if (!this.realSubject) { await this.mutex.acquire(); try { // Second check (with lock) if (!this.realSubject) { this.realSubject = new RealSubject(); } } finally { this.mutex.release(); } } return this.realSubject.request(); }} // SOLUTION 3: Eager initialization (simplest, if feasible)class EagerProxy implements Subject { private readonly realSubject = new RealSubject(); request(): Result { // No synchronization needed - already initialized return this.realSubject.request(); }}JavaScript is single-threaded (except for Workers), so race conditions in lazy initialization are rare in typical client code. However, when using async/await with external resources, interleaving can still cause issues. Server-side TypeScript with clustering or Workers requires full thread-safety consideration.
We've explored the structural foundation of the Proxy pattern and how surrogates provide controlled access to real subjects:
What's Next:
Now that we understand the basic structure, the next page dives deep into the different types of proxies: Virtual Proxy for lazy loading, Protection Proxy for access control, and Remote Proxy for location transparency. Each type applies the same structural pattern but serves distinct purposes.
You now understand how the Proxy pattern structures its solution—creating a surrogate that shares the same interface as the real subject. Next, we'll explore the specific proxy types and their implementations in detail.