Loading content...
While all proxies share the same structural foundation—a surrogate implementing the subject's interface—they serve fundamentally different purposes. The Gang of Four identified three primary proxy categories, each addressing distinct access control needs:
Understanding these categories helps you recognize which type your problem calls for and how to implement it correctly. Additionally, we'll explore a fourth category—Smart Reference—that adds intelligence to object access.
By the end of this page, you'll master each proxy type with comprehensive implementations. You'll understand when to use each type, their specific design considerations, and how they solve distinct categories of access control problems.
A Virtual Proxy creates expensive objects on demand. It acts as a placeholder for objects that are costly to create—either in terms of memory, time, or external dependencies.
Virtual proxies embody the principle of lazy evaluation: don't do work until the results are actually needed. This is particularly valuable when:
Let's build a production-quality virtual proxy for a document processing system:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
// Subject Interface - defines all document operationsinterface DocumentProcessor { readonly documentId: string; // Lightweight operations (can use metadata) getTitle(): string; getPageCount(): number; getMetadata(): DocumentMetadata; getThumbnail(): ImageData; // Heavy operations (require full document) getPageContent(pageNumber: number): PageContent; renderPage(pageNumber: number, options: RenderOptions): Canvas; searchContent(query: string): SearchResult[]; extractText(): string; exportToPDF(options: ExportOptions): Uint8Array;} interface DocumentMetadata { title: string; author: string; pageCount: number; createdAt: Date; modifiedAt: Date; fileSize: number; format: 'PDF' | 'DOCX' | 'ODT';} // RealSubject - expensive to create, holds all dataclass RealDocumentProcessor implements DocumentProcessor { private content: DocumentContent; private parsedPages: Map<number, PageContent> = new Map(); constructor(public readonly documentId: string) { console.log(`[RealDocumentProcessor] Loading document ${documentId}...`); // EXPENSIVE: Load entire document from storage const startTime = performance.now(); const rawData = FileSystem.read(`/documents/${documentId}`); // EXPENSIVE: Parse document structure this.content = DocumentParser.parse(rawData); // EXPENSIVE: Build search index this.buildSearchIndex(); console.log(`[RealDocumentProcessor] Document loaded in ${performance.now() - startTime}ms`); } private buildSearchIndex(): void { // CPU-intensive indexing operation for (let i = 0; i < this.content.pageCount; i++) { this.parsedPages.set(i, this.content.parsePage(i)); } } getTitle(): string { return this.content.metadata.title; } getPageCount(): number { return this.content.pageCount; } getMetadata(): DocumentMetadata { return this.content.metadata; } getThumbnail(): ImageData { return this.renderPage(0, { width: 150, height: 200 }).toImageData(); } getPageContent(pageNumber: number): PageContent { return this.parsedPages.get(pageNumber)!; } renderPage(pageNumber: number, options: RenderOptions): Canvas { const page = this.parsedPages.get(pageNumber)!; return Renderer.renderPage(page, options); } searchContent(query: string): SearchResult[] { const results: SearchResult[] = []; for (const [pageNum, content] of this.parsedPages) { const matches = content.search(query); results.push(...matches.map(m => ({ pageNumber: pageNum, ...m }))); } return results; } extractText(): string { let text = ''; for (const content of this.parsedPages.values()) { text += content.text + ' '; } return text; } exportToPDF(options: ExportOptions): Uint8Array { return PDFExporter.export(this.content, options); }}Now the Virtual Proxy that defers this expensive loading:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
// Virtual Proxy - defers expensive creationclass DocumentProcessorProxy implements DocumentProcessor { // Lightweight metadata - loaded immediately private metadata: DocumentMetadata | null = null; private thumbnail: ImageData | null = null; // Real processor - created only when needed private realProcessor: RealDocumentProcessor | null = null; private isLoading: boolean = false; private loadError: Error | null = null; constructor(public readonly documentId: string) { // Proxy is cheap - just stores the ID console.log(`[Proxy] Created lightweight proxy for ${documentId}`); } // Lazy initialization with error handling private ensureRealProcessor(): RealDocumentProcessor { if (this.loadError) { throw this.loadError; } if (!this.realProcessor) { try { console.log(`[Proxy] First heavy access - creating real processor`); this.realProcessor = new RealDocumentProcessor(this.documentId); } catch (error) { this.loadError = error as Error; throw error; } } return this.realProcessor; } // Async version for non-blocking loading private async ensureRealProcessorAsync(): Promise<RealDocumentProcessor> { if (this.loadError) { throw this.loadError; } if (!this.realProcessor && !this.isLoading) { this.isLoading = true; // Create in background return new Promise((resolve, reject) => { setTimeout(() => { try { this.realProcessor = new RealDocumentProcessor(this.documentId); this.isLoading = false; resolve(this.realProcessor); } catch (error) { this.loadError = error as Error; this.isLoading = false; reject(error); } }, 0); }); } // Wait if already loading while (this.isLoading) { await new Promise(r => setTimeout(r, 10)); } return this.realProcessor!; } // ===== LIGHTWEIGHT OPERATIONS ===== // These can be satisfied from metadata without loading full document getTitle(): string { return this.getMetadata().title; } getPageCount(): number { return this.getMetadata().pageCount; } getMetadata(): DocumentMetadata { if (!this.metadata) { // Read just metadata header - much cheaper than full load console.log(`[Proxy] Loading lightweight metadata`); this.metadata = MetadataReader.readQuick(`/documents/${this.documentId}`); } return this.metadata; } getThumbnail(): ImageData { if (!this.thumbnail) { // Try to read cached thumbnail first const cached = ThumbnailCache.get(this.documentId); if (cached) { this.thumbnail = cached; } else { // Only if no cache, trigger full load console.log(`[Proxy] No cached thumbnail - triggering full load`); this.thumbnail = this.ensureRealProcessor().getThumbnail(); ThumbnailCache.set(this.documentId, this.thumbnail); } } return this.thumbnail; } // ===== HEAVY OPERATIONS ===== // These require the full document to be loaded getPageContent(pageNumber: number): PageContent { console.log(`[Proxy] getPageContent requires full load`); return this.ensureRealProcessor().getPageContent(pageNumber); } renderPage(pageNumber: number, options: RenderOptions): Canvas { console.log(`[Proxy] renderPage requires full load`); return this.ensureRealProcessor().renderPage(pageNumber, options); } searchContent(query: string): SearchResult[] { console.log(`[Proxy] searchContent requires full load`); return this.ensureRealProcessor().searchContent(query); } extractText(): string { console.log(`[Proxy] extractText requires full load`); return this.ensureRealProcessor().extractText(); } exportToPDF(options: ExportOptions): Uint8Array { console.log(`[Proxy] exportToPDF requires full load`); return this.ensureRealProcessor().exportToPDF(options); } // ===== PROXY-SPECIFIC UTILITIES ===== /** Check if the heavy document is loaded */ isFullyLoaded(): boolean { return this.realProcessor !== null; } /** Pre-load in background for anticipated use */ async preload(): Promise<void> { if (!this.realProcessor) { await this.ensureRealProcessorAsync(); } } /** Release memory by unloading (can be re-loaded later) */ unload(): void { if (this.realProcessor) { console.log(`[Proxy] Unloading document to free memory`); this.realProcessor = null; } }}This virtual proxy provides: (1) Instant proxy creation, (2) Lightweight metadata access without full load, (3) Cached thumbnails, (4) On-demand full loading only when needed, (5) Background preloading capability, (6) Memory release through unload(). A document library can create 10,000 proxies instantly, with full documents loaded only for the few the user actually opens.
A Protection Proxy controls access to an object based on access rights. It acts as a security gatekeeper, checking permissions before allowing operations to proceed.
Protection proxies enforce authorization policies without cluttering business logic. They're essential in:
Let's build a comprehensive protection proxy for a banking system:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
// Types for our banking domaininterface User { id: string; name: string; roles: UserRole[]; department: string;} type UserRole = 'TELLER' | 'MANAGER' | 'ADMIN' | 'AUDITOR' | 'CUSTOMER'; interface AuditEntry { timestamp: Date; userId: string; action: string; accountNumber: string; details: Record<string, unknown>; outcome: 'SUCCESS' | 'DENIED' | 'ERROR';} // Subject Interfaceinterface BankAccount { readonly accountNumber: string; // Read operations getBalance(): Money; getTransactionHistory(days: number): Transaction[]; getAccountDetails(): AccountDetails; // Write operations deposit(amount: Money, description: string): TransactionResult; withdraw(amount: Money, description: string): TransactionResult; transfer(targetAccount: string, amount: Money, description: string): TransactionResult; // Administrative operations freeze(): void; unfreeze(): void; setDailyLimit(amount: Money): void; closeAccount(): CloseResult;} // RealSubject - actual bank account with no securityclass RealBankAccount implements BankAccount { private balance: Money; private transactions: Transaction[] = []; private isFrozen: boolean = false; private dailyLimit: Money = Money.fromDollars(10000); constructor( public readonly accountNumber: string, initialBalance: Money ) { this.balance = initialBalance; } getBalance(): Money { return this.balance; } getTransactionHistory(days: number): Transaction[] { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); return this.transactions.filter(t => t.timestamp >= cutoff); } getAccountDetails(): AccountDetails { return { accountNumber: this.accountNumber, balance: this.balance, isFrozen: this.isFrozen, dailyLimit: this.dailyLimit, openedDate: this.transactions[0]?.timestamp ?? new Date(), }; } deposit(amount: Money, description: string): TransactionResult { if (this.isFrozen) { return { success: false, error: 'Account is frozen' }; } this.balance = this.balance.add(amount); this.recordTransaction('DEPOSIT', amount, description); return { success: true, newBalance: this.balance }; } withdraw(amount: Money, description: string): TransactionResult { if (this.isFrozen) { return { success: false, error: 'Account is frozen' }; } if (amount.greaterThan(this.balance)) { return { success: false, error: 'Insufficient funds' }; } this.balance = this.balance.subtract(amount); this.recordTransaction('WITHDRAWAL', amount, description); return { success: true, newBalance: this.balance }; } transfer(targetAccount: string, amount: Money, description: string): TransactionResult { const withdrawResult = this.withdraw(amount, `Transfer to ${targetAccount}: ${description}`); if (!withdrawResult.success) { return withdrawResult; } // In real system, would interact with target account return { success: true, newBalance: this.balance }; } freeze(): void { this.isFrozen = true; } unfreeze(): void { this.isFrozen = false; } setDailyLimit(amount: Money): void { this.dailyLimit = amount; } closeAccount(): CloseResult { if (this.balance.greaterThan(Money.zero())) { return { success: false, error: 'Account has remaining balance' }; } return { success: true }; } private recordTransaction(type: string, amount: Money, description: string): void { this.transactions.push({ id: generateId(), timestamp: new Date(), type, amount, description, }); }}Now the Protection Proxy with comprehensive access control:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
// Permission definitionsconst PERMISSIONS = { // Read permissions VIEW_BALANCE: ['TELLER', 'MANAGER', 'ADMIN', 'AUDITOR', 'CUSTOMER'], VIEW_HISTORY: ['TELLER', 'MANAGER', 'ADMIN', 'AUDITOR', 'CUSTOMER'], VIEW_DETAILS: ['MANAGER', 'ADMIN', 'AUDITOR'], // Write permissions DEPOSIT: ['TELLER', 'MANAGER', 'ADMIN'], WITHDRAW: ['TELLER', 'MANAGER', 'ADMIN'], TRANSFER: ['TELLER', 'MANAGER', 'ADMIN'], // Admin permissions FREEZE: ['MANAGER', 'ADMIN'], UNFREEZE: ['ADMIN'], // Only admin can unfreeze SET_LIMIT: ['MANAGER', 'ADMIN'], CLOSE_ACCOUNT: ['ADMIN'],} as const; type Permission = keyof typeof PERMISSIONS; // Protection Proxy with comprehensive securityclass SecureBankAccountProxy implements BankAccount { private auditLog: AuditEntry[] = []; constructor( private readonly realAccount: RealBankAccount, private readonly currentUser: User, private readonly auditService: AuditService ) {} get accountNumber(): string { return this.realAccount.accountNumber; } // ===== PERMISSION CHECKING ===== private hasPermission(permission: Permission): boolean { const allowedRoles = PERMISSIONS[permission]; return this.currentUser.roles.some(role => allowedRoles.includes(role) ); } private checkPermission(permission: Permission, action: string): void { if (!this.hasPermission(permission)) { this.audit(action, { permission }, 'DENIED'); throw new UnauthorizedError( `User ${this.currentUser.name} lacks permission: ${permission}` ); } } private audit( action: string, details: Record<string, unknown>, outcome: 'SUCCESS' | 'DENIED' | 'ERROR' ): void { const entry: AuditEntry = { timestamp: new Date(), userId: this.currentUser.id, action, accountNumber: this.accountNumber, details, outcome, }; this.auditService.log(entry); } // ===== READ OPERATIONS ===== getBalance(): Money { this.checkPermission('VIEW_BALANCE', 'getBalance'); const result = this.realAccount.getBalance(); this.audit('getBalance', { balance: result.toString() }, 'SUCCESS'); return result; } getTransactionHistory(days: number): Transaction[] { this.checkPermission('VIEW_HISTORY', 'getTransactionHistory'); // Additional check: customers can only see 90 days const effectiveDays = this.currentUser.roles.includes('CUSTOMER') ? Math.min(days, 90) : days; const result = this.realAccount.getTransactionHistory(effectiveDays); this.audit('getTransactionHistory', { requestedDays: days, effectiveDays, resultCount: result.length }, 'SUCCESS'); return result; } getAccountDetails(): AccountDetails { this.checkPermission('VIEW_DETAILS', 'getAccountDetails'); const result = this.realAccount.getAccountDetails(); this.audit('getAccountDetails', {}, 'SUCCESS'); return result; } // ===== WRITE OPERATIONS ===== deposit(amount: Money, description: string): TransactionResult { this.checkPermission('DEPOSIT', 'deposit'); // Additional validation if (amount.greaterThan(Money.fromDollars(50000))) { // Large deposits require manager approval if (!this.currentUser.roles.includes('MANAGER') && !this.currentUser.roles.includes('ADMIN')) { this.audit('deposit', { amount: amount.toString(), reason: 'NEEDS_APPROVAL' }, 'DENIED'); throw new ApprovalRequiredError('Deposits over $50,000 require manager approval'); } } const result = this.realAccount.deposit(amount, description); this.audit('deposit', { amount: amount.toString(), description, success: result.success }, result.success ? 'SUCCESS' : 'ERROR'); return result; } withdraw(amount: Money, description: string): TransactionResult { this.checkPermission('WITHDRAW', 'withdraw'); // Rate limiting: max 5 withdrawals per hour per teller if (this.currentUser.roles.includes('TELLER')) { const recentWithdrawals = this.auditService.countRecent( this.currentUser.id, 'withdraw', 60 // minutes ); if (recentWithdrawals >= 5) { this.audit('withdraw', { reason: 'RATE_LIMITED' }, 'DENIED'); throw new RateLimitError('Maximum withdrawal operations exceeded'); } } const result = this.realAccount.withdraw(amount, description); this.audit('withdraw', { amount: amount.toString(), description, success: result.success }, result.success ? 'SUCCESS' : 'ERROR'); return result; } transfer(targetAccount: string, amount: Money, description: string): TransactionResult { this.checkPermission('TRANSFER', 'transfer'); // Large transfers require dual authorization if (amount.greaterThan(Money.fromDollars(100000))) { const hasSecondApproval = this.checkDualAuthorization(targetAccount, amount); if (!hasSecondApproval) { this.audit('transfer', { targetAccount, amount: amount.toString(), reason: 'NEEDS_DUAL_AUTH' }, 'DENIED'); throw new DualAuthorizationError( 'Transfers over $100,000 require dual authorization' ); } } const result = this.realAccount.transfer(targetAccount, amount, description); this.audit('transfer', { targetAccount, amount: amount.toString(), description, success: result.success }, result.success ? 'SUCCESS' : 'ERROR'); return result; } // ===== ADMIN OPERATIONS ===== freeze(): void { this.checkPermission('FREEZE', 'freeze'); this.realAccount.freeze(); this.audit('freeze', { reason: 'Manual freeze by authorized user' }, 'SUCCESS'); // Trigger notification NotificationService.notify( `Account ${this.accountNumber} frozen by ${this.currentUser.name}` ); } unfreeze(): void { this.checkPermission('UNFREEZE', 'unfreeze'); this.realAccount.unfreeze(); this.audit('unfreeze', { reason: 'Manual unfreeze by admin' }, 'SUCCESS'); } setDailyLimit(amount: Money): void { this.checkPermission('SET_LIMIT', 'setDailyLimit'); const previousLimit = this.realAccount.getAccountDetails().dailyLimit; this.realAccount.setDailyLimit(amount); this.audit('setDailyLimit', { previousLimit: previousLimit.toString(), newLimit: amount.toString() }, 'SUCCESS'); } closeAccount(): CloseResult { this.checkPermission('CLOSE_ACCOUNT', 'closeAccount'); const result = this.realAccount.closeAccount(); this.audit('closeAccount', { success: result.success }, result.success ? 'SUCCESS' : 'ERROR'); return result; } private checkDualAuthorization(target: string, amount: Money): boolean { // Check if another authorized user has approved this specific transfer return DualAuthService.hasApproval( this.accountNumber, target, amount ); }}The protection proxy centralizes ALL security logic: (1) Role-based permissions, (2) Business rules (large transaction limits), (3) Rate limiting, (4) Dual authorization for sensitive operations, (5) Complete audit trail. The RealBankAccount knows nothing about security—it just does banking. Security is a separate concern, cleanly encapsulated.
A Remote Proxy provides a local representative for an object in a different address space. It handles all the complexity of network communication—serialization, transport, error handling—presenting a simple local interface to clients.
Remote proxies implement location transparency: the client doesn't know (or need to know) that the object is remote. This is foundational to distributed systems:
Let's build a remote proxy for a distributed inventory service:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
// Subject Interface - same for local and remoteinterface InventoryService { // Query operations checkStock(productId: string): Promise<StockInfo>; getProduct(productId: string): Promise<Product | null>; searchProducts(criteria: SearchCriteria): Promise<ProductSearchResult>; // Mutation operations reserveStock(productId: string, quantity: number): Promise<ReservationResult>; releaseReservation(reservationId: string): Promise<void>; updateStock(productId: string, delta: number, reason: string): Promise<StockInfo>;} interface StockInfo { productId: string; available: number; reserved: number; inTransit: number; lastUpdated: Date;} interface ReservationResult { success: boolean; reservationId?: string; expiresAt?: Date; error?: string;} // Network layer typesinterface RpcRequest { id: string; method: string; params: unknown[]; timestamp: number;} interface RpcResponse<T = unknown> { id: string; result?: T; error?: RpcError;} interface RpcError { code: number; message: string; data?: unknown;} // Remote Proxy Implementationclass InventoryServiceProxy implements InventoryService { private readonly requestTimeout = 30000; // 30 seconds private readonly maxRetries = 3; // Circuit breaker state private failureCount = 0; private lastFailureTime = 0; private circuitOpen = false; private readonly failureThreshold = 5; private readonly resetTimeout = 60000; // 1 minute constructor( private readonly serviceUrl: string, private readonly loadBalancer: LoadBalancer, private readonly connectionPool: ConnectionPool ) {} // ===== PUBLIC INTERFACE METHODS ===== async checkStock(productId: string): Promise<StockInfo> { return this.callRemote<StockInfo>('checkStock', [productId]); } async getProduct(productId: string): Promise<Product | null> { return this.callRemote<Product | null>('getProduct', [productId]); } async searchProducts(criteria: SearchCriteria): Promise<ProductSearchResult> { return this.callRemote<ProductSearchResult>('searchProducts', [criteria]); } async reserveStock(productId: string, quantity: number): Promise<ReservationResult> { // Mutations don't retry to avoid duplicates return this.callRemote<ReservationResult>( 'reserveStock', [productId, quantity], { retryable: false } ); } async releaseReservation(reservationId: string): Promise<void> { await this.callRemote<void>('releaseReservation', [reservationId]); } async updateStock( productId: string, delta: number, reason: string ): Promise<StockInfo> { return this.callRemote<StockInfo>( 'updateStock', [productId, delta, reason], { retryable: false } ); } // ===== REMOTE CALL INFRASTRUCTURE ===== private async callRemote<T>( method: string, params: unknown[], options: { retryable?: boolean } = {} ): Promise<T> { const { retryable = true } = options; // Check circuit breaker if (this.isCircuitOpen()) { throw new ServiceUnavailableError( `Inventory service circuit breaker open. Try again later.` ); } const request: RpcRequest = { id: generateRequestId(), method, params, timestamp: Date.now(), }; let lastError: Error | null = null; const attempts = retryable ? this.maxRetries : 1; for (let attempt = 1; attempt <= attempts; attempt++) { try { const response = await this.sendRequest<T>(request); // Success - reset circuit breaker this.recordSuccess(); return response; } catch (error) { lastError = error as Error; console.warn(`[RemoteProxy] Attempt ${attempt} failed: ${lastError.message}`); // Don't retry non-retryable operations if (!retryable || !this.isRetryableError(error)) { break; } // Exponential backoff if (attempt < attempts) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); await this.sleep(delay); } } } // Record failure for circuit breaker this.recordFailure(); throw new RemoteCallError( `Remote call to ${method} failed after ${attempts} attempts`, lastError ); } private async sendRequest<T>(request: RpcRequest): Promise<T> { // Get connection from pool const connection = await this.connectionPool.acquire(); try { // Select server via load balancer const server = await this.loadBalancer.selectServer(); // Serialize request const serialized = JSON.stringify(request); // Send with timeout const responsePromise = connection.send(server, serialized); const timeoutPromise = this.createTimeout(request.id); const rawResponse = await Promise.race([ responsePromise, timeoutPromise, ]) as string; // Deserialize response const response = JSON.parse(rawResponse) as RpcResponse<T>; // Check for RPC-level errors if (response.error) { throw new RpcCallError( response.error.code, response.error.message, response.error.data ); } return response.result as T; } finally { // Always return connection to pool this.connectionPool.release(connection); } } // ===== CIRCUIT BREAKER ===== private isCircuitOpen(): boolean { if (!this.circuitOpen) { return false; } // Check if reset timeout has passed if (Date.now() - this.lastFailureTime > this.resetTimeout) { console.log('[RemoteProxy] Circuit breaker half-open, allowing test request'); this.circuitOpen = false; this.failureCount = 0; return false; } return true; } private recordSuccess(): void { this.failureCount = 0; this.circuitOpen = false; } private recordFailure(): void { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { console.error(`[RemoteProxy] Circuit breaker opened after ${this.failureCount} failures`); this.circuitOpen = true; } } // ===== UTILITIES ===== private isRetryableError(error: unknown): boolean { if (error instanceof RpcCallError) { // Don't retry business logic errors return error.code >= 500; // Only server errors } // Network errors are retryable return error instanceof NetworkError || error instanceof TimeoutError; } private createTimeout(requestId: string): Promise<never> { return new Promise((_, reject) => { setTimeout(() => { reject(new TimeoutError(`Request ${requestId} timed out`)); }, this.requestTimeout); }); } private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }}Client code calls inventoryService.checkStock('SKU-123') without knowing it's remote. The proxy handles: serialization, load balancing, connection pooling, timeouts, retries with exponential backoff, and circuit breaking. This is production-grade distributed systems infrastructure hidden behind a simple interface.
A Smart Reference Proxy provides additional actions when an object is accessed. Unlike virtual, protection, or remote proxies that control WHETHER access happens, smart references add behavior TO access operations.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
// Caching Proxy with TTL and cache invalidationclass CachingServiceProxy<T extends object> { private cache: Map<string, CacheEntry<unknown>> = new Map(); constructor( private readonly realService: T, private readonly defaultTTL: number = 60000, // 1 minute private readonly maxCacheSize: number = 1000 ) { return this.createProxy(); } private createProxy(): T { return new Proxy(this.realService, { get: (target, prop, receiver) => { const value = Reflect.get(target, prop, receiver); if (typeof value !== 'function') { return value; } // Return wrapped function that checks cache return async (...args: unknown[]) => { const cacheKey = this.buildCacheKey(prop as string, args); // Check cache const cached = this.getFromCache(cacheKey); if (cached !== undefined) { console.log(`[Cache] HIT: ${cacheKey}`); return cached; } // Cache miss - call real service console.log(`[Cache] MISS: ${cacheKey}`); const result = await value.apply(target, args); // Store in cache this.setInCache(cacheKey, result); return result; }; } }) as T; } private buildCacheKey(method: string, args: unknown[]): string { return `${method}:${JSON.stringify(args)}`; } private getFromCache(key: string): unknown | undefined { const entry = this.cache.get(key); if (!entry) return undefined; if (Date.now() > entry.expiresAt) { this.cache.delete(key); return undefined; } return entry.value; } private setInCache(key: string, value: unknown, ttl?: number): void { // Evict oldest entries if at capacity if (this.cache.size >= this.maxCacheSize) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } this.cache.set(key, { value, expiresAt: Date.now() + (ttl ?? this.defaultTTL), createdAt: Date.now(), }); } // Public cache management methods invalidate(pattern?: string): void { if (!pattern) { this.cache.clear(); return; } for (const key of this.cache.keys()) { if (key.includes(pattern)) { this.cache.delete(key); } } } getStats(): CacheStats { return { size: this.cache.size, maxSize: this.maxCacheSize, }; }} interface CacheEntry<T> { value: T; expiresAt: number; createdAt: number;} // Usageconst weatherService = new CachingServiceProxy( new RealWeatherService(), 300000 // 5 minute TTL for weather data); // First call hits APIconst weather1 = await weatherService.getCurrentWeather('NYC'); // Second call returns cached result instantlyconst weather2 = await weatherService.getCurrentWeather('NYC');123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
// Comprehensive logging proxy with performance metricsfunction createLoggingProxy<T extends object>( target: T, serviceName: string, options: LoggingOptions = {}): T { const { logLevel = 'info', includeArgs = true, includeResult = false, performanceThresholdMs = 1000, } = options; return new Proxy(target, { get(obj, prop, receiver) { const value = Reflect.get(obj, prop, receiver); if (typeof value !== 'function') { return value; } return function(...args: unknown[]) { const callId = generateCallId(); const methodName = String(prop); const startTime = performance.now(); // Log entry Logger[logLevel]({ event: 'method_call_start', callId, service: serviceName, method: methodName, args: includeArgs ? sanitizeArgs(args) : undefined, timestamp: new Date().toISOString(), }); try { const result = value.apply(obj, args); // Handle promises if (result instanceof Promise) { return result .then(asyncResult => { logSuccess(callId, methodName, startTime, asyncResult); return asyncResult; }) .catch(error => { logError(callId, methodName, startTime, error); throw error; }); } // Synchronous result logSuccess(callId, methodName, startTime, result); return result; } catch (error) { logError(callId, methodName, startTime, error as Error); throw error; } }; function logSuccess( callId: string, method: string, startTime: number, result: unknown ): void { const duration = performance.now() - startTime; const entry = { event: 'method_call_success', callId, service: serviceName, method, durationMs: Math.round(duration * 100) / 100, result: includeResult ? sanitizeResult(result) : undefined, }; if (duration > performanceThresholdMs) { Logger.warn({ ...entry, slow: true, threshold: performanceThresholdMs, }); } else { Logger[logLevel](entry); } } function logError( callId: string, method: string, startTime: number, error: Error ): void { Logger.error({ event: 'method_call_error', callId, service: serviceName, method, durationMs: Math.round((performance.now() - startTime) * 100) / 100, error: { name: error.name, message: error.message, stack: error.stack, }, }); } } }) as T;} // Usage - wrap any service with comprehensive loggingconst orderService = createLoggingProxy( new OrderService(), 'OrderService', { logLevel: 'debug', performanceThresholdMs: 500, }); // All calls now automatically loggedawait orderService.createOrder(order);// Logs: { event: 'method_call_start', service: 'OrderService', method: 'createOrder', ... }// Logs: { event: 'method_call_success', durationMs: 42.35, ... }Understanding when to use each proxy type is essential for applying the pattern correctly:
| Aspect | Virtual Proxy | Protection Proxy | Remote Proxy | Smart Reference |
|---|---|---|---|---|
| Primary Purpose | Defer expensive creation | Enforce access control | Abstract network location | Add behavior to access |
| When Created | When proxy is instantiated | When proxy wraps subject | At application startup | When wrapping existing object |
| Subject Location | Same process | Same process | Different process/machine | Same process |
| Common Features | Lazy loading, memory management | Auth, audit, rate limiting | Serialization, retry, circuit breaking | Caching, logging, metrics |
| Performance Impact | Reduces memory + startup time | Minimal (permission checks) | Adds network latency | Varies by feature |
| Transparency | Full (client unaware) | May throw access errors | May throw network errors | Full (with enhanced behavior) |
Proxy types can be combined! A remote service might have a Remote Proxy for network communication, wrapped in a Caching Proxy for performance, wrapped in a Protection Proxy for security. Each layer adds its concern without affecting others.
12345678910111213141516171819202122232425262728293031
// Combining multiple proxy typesfunction createProductService(user: User): ProductService { // Layer 1: Remote proxy handles network communication const remoteProxy = new ProductServiceRemoteProxy( 'https://products.api.internal:8080' ); // Layer 2: Caching proxy adds performance optimization const cachingProxy = new CachingServiceProxy( remoteProxy, 60000 // 1 minute cache ); // Layer 3: Protection proxy adds security (outermost layer) const protectionProxy = new ProductServiceSecurityProxy( cachingProxy, user ); return protectionProxy;} // Client uses simple interface, unaware of all the infrastructureconst productService = createProductService(currentUser);const product = await productService.getProduct('SKU-123'); // Behind the scenes:// 1. Protection proxy checks user.canRead('products')// 2. Caching proxy checks if 'SKU-123' is cached// 3. If not cached, remote proxy makes HTTP call with retry/circuit breaker// 4. Result flows back through layers, getting cached along the wayWhat's Next:
The final page brings everything together with comprehensive real-world examples. We'll see proxy patterns in action across multiple domains—from image galleries to API gateways to database connection pools—solidifying your ability to apply proxies effectively in production systems.
You now have deep knowledge of all major proxy types with production-quality implementation patterns. Next, we'll explore real-world use cases and examples that demonstrate these patterns in action.