← Back to principles

Design Principle

Proxy Pattern

Learn the Proxy pattern: provide a surrogate or placeholder for another object to control access to it. Understand virtual, protection, and remote proxies.

The Proxy Pattern provides a surrogate or placeholder for another object to control access to it. The proxy acts as an intermediary between the client and the real object.


What is the Proxy Pattern?

Proxy Pattern provides:

  • Access control: Control access to the real object
  • Lazy loading: Defer expensive object creation
  • Caching: Cache results from expensive operations
  • Security: Add security checks before access

Types:

  • Virtual Proxy: Lazy loading
  • Protection Proxy: Access control
  • Remote Proxy: Network communication
  • Caching Proxy: Cache results

Structure

Subject (interface)
  └─ request()

RealSubject (implements Subject)
  └─ request()

Proxy (implements Subject)
  └─ realSubject: RealSubject
  └─ request() (controls access, calls realSubject.request())

Examples

Virtual Proxy (Lazy Loading)

// Subject interface
interface Image {
  display(): void;
}

// Real subject
class RealImage implements Image {
  private filename: string;
  
  constructor(filename: string) {
    this.filename = filename;
    this.loadFromDisk(); // Expensive operation
  }
  
  private loadFromDisk(): void {
    console.log(`Loading ${this.filename} from disk...`);
    // Simulate expensive loading
  }
  
  display(): void {
    console.log(`Displaying ${this.filename}`);
  }
}

// Proxy (lazy loading)
class ProxyImage implements Image {
  private realImage: RealImage | null = null;
  private filename: string;
  
  constructor(filename: string) {
    this.filename = filename;
  }
  
  display(): void {
    if (this.realImage === null) {
      this.realImage = new RealImage(this.filename); // Lazy loading
    }
    this.realImage.display();
  }
}

// Usage
const image = new ProxyImage("photo.jpg");
// Image not loaded yet

image.display();
// Now image is loaded and displayed

Protection Proxy (Access Control)

// Subject interface
interface BankAccount {
  deposit(amount: number): void;
  withdraw(amount: number): void;
  getBalance(): number;
}

// Real subject
class RealBankAccount implements BankAccount {
  private balance: number = 0;
  
  deposit(amount: number): void {
    this.balance += amount;
    console.log(`Deposited ${amount}. Balance: ${this.balance}`);
  }
  
  withdraw(amount: number): void {
    if (this.balance >= amount) {
      this.balance -= amount;
      console.log(`Withdrew ${amount}. Balance: ${this.balance}`);
    } else {
      console.log("Insufficient funds");
    }
  }
  
  getBalance(): number {
    return this.balance;
  }
}

// Protection proxy
class ProtectionProxy implements BankAccount {
  private realAccount: RealBankAccount;
  private user: User;
  
  constructor(user: User) {
    this.user = user;
    this.realAccount = new RealBankAccount();
  }
  
  deposit(amount: number): void {
    if (this.hasPermission("deposit")) {
      this.realAccount.deposit(amount);
    } else {
      throw new Error("Permission denied");
    }
  }
  
  withdraw(amount: number): void {
    if (this.hasPermission("withdraw")) {
      this.realAccount.withdraw(amount);
    } else {
      throw new Error("Permission denied");
    }
  }
  
  getBalance(): number {
    if (this.hasPermission("view")) {
      return this.realAccount.getBalance();
    } else {
      throw new Error("Permission denied");
    }
  }
  
  private hasPermission(action: string): boolean {
    // Check user permissions
    return this.user.permissions.includes(action);
  }
}

// Usage
const user = { permissions: ["deposit", "view"] };
const account = new ProtectionProxy(user);

account.deposit(100); // Allowed
account.getBalance(); // Allowed
account.withdraw(50); // Throws error (no permission)

Caching Proxy

// Subject interface
interface DataService {
  fetchData(key: string): Promise<string>;
}

// Real subject
class RealDataService implements DataService {
  async fetchData(key: string): Promise<string> {
    console.log(`Fetching data for key: ${key}`);
    // Simulate expensive network call
    await new Promise(resolve => setTimeout(resolve, 1000));
    return `Data for ${key}`;
  }
}

// Caching proxy
class CachingProxy implements DataService {
  private realService: RealDataService;
  private cache: Map<string, string> = new Map();
  
  constructor() {
    this.realService = new RealDataService();
  }
  
  async fetchData(key: string): Promise<string> {
    // Check cache first
    if (this.cache.has(key)) {
      console.log(`Cache hit for key: ${key}`);
      return this.cache.get(key)!;
    }
    
    // Cache miss - fetch from real service
    console.log(`Cache miss for key: ${key}`);
    const data = await this.realService.fetchData(key);
    this.cache.set(key, data);
    return data;
  }
  
  clearCache(): void {
    this.cache.clear();
  }
}

// Usage
const service = new CachingProxy();

// First call - cache miss
await service.fetchData("key1");

// Second call - cache hit
await service.fetchData("key1");

Remote Proxy

// Subject interface
interface RemoteService {
  processRequest(data: string): Promise<string>;
}

// Real subject (on remote server)
class RemoteServiceImpl implements RemoteService {
  async processRequest(data: string): Promise<string> {
    // Process on remote server
    return `Processed: ${data}`;
  }
}

// Remote proxy (client-side)
class RemoteProxy implements RemoteService {
  private serverUrl: string;
  
  constructor(serverUrl: string) {
    this.serverUrl = serverUrl;
  }
  
  async processRequest(data: string): Promise<string> {
    // Make network call to remote server
    const response = await fetch(`${this.serverUrl}/process`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ data })
    });
    
    const result = await response.json();
    return result.processed;
  }
}

// Usage
const proxy = new RemoteProxy("https://api.example.com");
const result = await proxy.processRequest("test data");

Common Pitfalls

  • Over-proxying: Too many proxy layers. Fix: Use only when needed
  • Performance overhead: Proxy adds overhead. Fix: Consider performance impact
  • Tight coupling: Proxy tightly coupled to real subject. Fix: Use interfaces
  • Not handling errors: Proxy doesn't handle errors properly. Fix: Add error handling

Interview Questions

Beginner

Q: What is the Proxy pattern and what are its use cases?

A:

Proxy Pattern provides a surrogate or placeholder for another object to control access to it.

Key characteristics:

  • Surrogate: Acts as intermediary between client and real object
  • Access control: Controls access to real object
  • Same interface: Implements same interface as real subject

Types:

  • Virtual Proxy: Lazy loading (defer expensive creation)
  • Protection Proxy: Access control (security checks)
  • Remote Proxy: Network communication (remote objects)
  • Caching Proxy: Cache results (avoid repeated operations)

Use cases:

  • Lazy loading: Defer expensive object creation
  • Access control: Add security checks
  • Caching: Cache expensive operations
  • Remote access: Access remote objects locally

Example:

// Proxy controls access to real image
class ProxyImage implements Image {
  private realImage: RealImage | null = null;
  
  display(): void {
    if (this.realImage === null) {
      this.realImage = new RealImage(); // Lazy loading
    }
    this.realImage.display();
  }
}

Intermediate

Q: Explain different types of proxies. How do you implement a virtual proxy for lazy loading?

A:

Types of Proxies:

1. Virtual Proxy (Lazy Loading):

class ProxyImage implements Image {
  private realImage: RealImage | null = null;
  
  display(): void {
    if (this.realImage === null) {
      this.realImage = new RealImage(); // Load only when needed
    }
    this.realImage.display();
  }
}

Use: Defer expensive object creation

2. Protection Proxy (Access Control):

class ProtectionProxy implements BankAccount {
  deposit(amount: number): void {
    if (this.hasPermission()) {
      this.realAccount.deposit(amount);
    } else {
      throw new Error("Permission denied");
    }
  }
}

Use: Add security checks

3. Remote Proxy:

class RemoteProxy implements Service {
  async process(data: string): Promise<string> {
    // Make network call
    return await fetch("/api/process", { body: data });
  }
}

Use: Access remote objects locally

4. Caching Proxy:

class CachingProxy implements DataService {
  async fetch(key: string): Promise<string> {
    if (this.cache.has(key)) {
      return this.cache.get(key); // Return cached
    }
    const data = await this.realService.fetch(key);
    this.cache.set(key, data);
    return data;
  }
}

Use: Cache expensive operations


Senior

Q: Design a proxy system for a distributed caching layer that handles cache invalidation, load balancing, and failover. How do you ensure consistency and handle network failures?

A:

// Subject interface
interface CacheService {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttl: number): Promise<void>;
  delete(key: string): Promise<void>;
}

// Real cache service (Redis, Memcached, etc.)
class RealCacheService implements CacheService {
  async get(key: string): Promise<string | null> {
    // Direct cache access
    return await this.cache.get(key);
  }
  
  async set(key: string, value: string, ttl: number): Promise<void> {
    await this.cache.set(key, value, ttl);
  }
  
  async delete(key: string): Promise<void> {
    await this.cache.delete(key);
  }
}

// Distributed cache proxy
class DistributedCacheProxy implements CacheService {
  private servers: CacheService[];
  private currentServer: number = 0;
  private localCache: Map<string, { value: string, expires: number }> = new Map();
  
  constructor(servers: CacheService[]) {
    this.servers = servers;
  }
  
  async get(key: string): Promise<string | null> {
    // Check local cache first
    const local = this.localCache.get(key);
    if (local && local.expires > Date.now()) {
      return local.value;
    }
    
    // Try servers with failover
    for (let i = 0; i < this.servers.length; i++) {
      const server = this.getServer();
      try {
        const value = await this.withTimeout(
          server.get(key),
          1000 // 1 second timeout
        );
        
        if (value !== null) {
          // Cache locally
          this.localCache.set(key, {
            value,
            expires: Date.now() + 60000 // 1 minute local cache
          });
          return value;
        }
      } catch (error) {
        // Server failed, try next
        this.rotateServer();
        continue;
      }
    }
    
    return null;
  }
  
  async set(key: string, value: string, ttl: number): Promise<void> {
    // Write to all servers (for consistency)
    const promises = this.servers.map(server =>
      this.withTimeout(server.set(key, value, ttl), 1000)
        .catch(error => {
          console.error(`Server write failed: ${error}`);
        })
    );
    
    await Promise.all(promises);
    
    // Update local cache
    this.localCache.set(key, {
      value,
      expires: Date.now() + ttl * 1000
    });
  }
  
  async delete(key: string): Promise<void> {
    // Delete from all servers
    await Promise.all(
      this.servers.map(server =>
        this.withTimeout(server.delete(key), 1000)
      )
    );
    
    // Remove from local cache
    this.localCache.delete(key);
  }
  
  private getServer(): CacheService {
    // Round-robin load balancing
    return this.servers[this.currentServer];
  }
  
  private rotateServer(): void {
    this.currentServer = (this.currentServer + 1) % this.servers.length;
  }
  
  private async withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
    return Promise.race([
      promise,
      new Promise<T>((_, reject) =>
        setTimeout(() => reject(new Error("Timeout")), timeout)
      )
    ]);
  }
  
  // Cache invalidation
  invalidate(pattern: string): void {
    // Invalidate matching keys
    for (const key of this.localCache.keys()) {
      if (this.matchesPattern(key, pattern)) {
        this.localCache.delete(key);
      }
    }
  }
  
  private matchesPattern(key: string, pattern: string): boolean {
    // Simple pattern matching
    const regex = new RegExp(pattern.replace("*", ".*"));
    return regex.test(key);
  }
}

// Usage
const servers = [
  new RealCacheService("redis://server1"),
  new RealCacheService("redis://server2"),
  new RealCacheService("redis://server3")
];

const proxy = new DistributedCacheProxy(servers);
await proxy.set("key1", "value1", 3600);
const value = await proxy.get("key1");

Features:

  1. Load balancing: Round-robin across servers
  2. Failover: Try next server on failure
  3. Local caching: Cache locally for performance
  4. Consistency: Write to all servers
  5. Timeout handling: Handle network timeouts
  6. Cache invalidation: Invalidate by pattern

Key Takeaways

  • Proxy pattern: Provides surrogate to control access to object
  • Types: Virtual (lazy loading), Protection (access control), Remote (network), Caching (cache)
  • Benefits: Access control, lazy loading, caching, security
  • Use cases: Lazy loading, access control, remote access, caching
  • Best practices: Use interfaces, handle errors, consider performance

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.