Episode 9 — System Design / 9.4 — Structural Design Patterns

9.4.c -- Proxy Pattern

In one sentence: The Proxy pattern provides a surrogate or placeholder for another object to control access to it -- like a security guard who stands between you and a VIP, deciding whether to let you through, making you wait, or logging your visit.

Navigation: <- 9.4.b -- Facade | 9.4.d -- Decorator ->


Table of Contents


1. What is the Proxy Pattern?

A Proxy is an object that has the same interface as the real object but intercepts access to it. The client calls the proxy thinking it is the real object; the proxy decides what happens before (or instead of) forwarding the call.

  ┌────────┐         ┌───────────┐         ┌──────────────┐
  │ Client │────────>│   Proxy   │────────>│ Real Object  │
  │        │         │           │         │ (Subject)    │
  │ calls  │         │ same      │         │              │
  │ doWork()         │ interface │         │ doWork()     │
  │        │         │           │         │ (actual      │
  │        │         │ controls  │         │  logic)      │
  │        │         │ access    │         │              │
  └────────┘         └───────────┘         └──────────────┘
                     
  The client does not know (or care) whether
  it is talking to the proxy or the real object.

Critical distinction from other patterns:

PatternSame interface?What it does
ProxyYesControls access to the same object
AdapterNo (translates)Changes the interface
DecoratorYesAdds new behavior
FacadeNo (simplifies)Simplifies a subsystem

2. Types of Proxies

  ┌─────────────────────────────────────────────────────────┐
  │                    PROXY TYPES                          │
  │                                                         │
  │  VIRTUAL PROXY      Lazy loading / deferred creation    │
  │  "Don't create the expensive object until needed"       │
  │                                                         │
  │  PROTECTION PROXY   Access control / authorization      │
  │  "Check permissions before allowing the operation"      │
  │                                                         │
  │  CACHING PROXY      Store and reuse results             │
  │  "If we already computed this, return the cached copy"  │
  │                                                         │
  │  LOGGING PROXY      Audit trail / monitoring            │
  │  "Record every access for debugging or analytics"       │
  │                                                         │
  │  REMOTE PROXY       Network transparency                │
  │  "Make a remote object look like a local one"           │
  │                                                         │
  └─────────────────────────────────────────────────────────┘

3. Virtual Proxy -- Lazy Loading

The virtual proxy delays creating an expensive object until someone actually uses it.

// ============================================================
// VIRTUAL PROXY -- LAZY LOADING
// ============================================================

// ---- Expensive real object ----

class HeavyImage {
  constructor(filename) {
    this.filename = filename;
    this.data = null;
    this._loadFromDisk(filename);
  }

  _loadFromDisk(filename) {
    // Simulates expensive operation (loading a 50MB image)
    console.log(`  [HeavyImage] Loading ${filename} from disk... (expensive!)`);
    this.data = `<<binary data for ${filename}>>`;
    this.width = 3840;
    this.height = 2160;
  }

  display() {
    console.log(`  [HeavyImage] Displaying ${this.filename} (${this.width}x${this.height})`);
  }

  getMetadata() {
    return { filename: this.filename, width: this.width, height: this.height };
  }
}


// ---- Virtual Proxy: delays loading until display() or getMetadata() ----

class ImageProxy {
  constructor(filename) {
    this.filename = filename;
    this._realImage = null; // NOT loaded yet
    console.log(`  [ImageProxy] Proxy created for ${filename} (no disk I/O yet)`);
  }

  _loadIfNeeded() {
    if (!this._realImage) {
      console.log(`  [ImageProxy] First access -- loading real image now`);
      this._realImage = new HeavyImage(this.filename);
    }
  }

  display() {
    this._loadIfNeeded();
    this._realImage.display();
  }

  getMetadata() {
    this._loadIfNeeded();
    return this._realImage.getMetadata();
  }
}


// ---- Usage ----

console.log('=== Creating 3 image proxies (no disk I/O) ===');
const images = [
  new ImageProxy('photo1.jpg'),
  new ImageProxy('photo2.jpg'),
  new ImageProxy('photo3.jpg'),
];
// At this point: zero disk I/O has occurred

console.log('\n=== User scrolls to first image ===');
images[0].display(); // NOW it loads from disk

console.log('\n=== User scrolls to first image again ===');
images[0].display(); // Already loaded -- no disk I/O

console.log('\n=== Images 2 and 3 never viewed -- never loaded ===');

Output:

=== Creating 3 image proxies (no disk I/O) ===
  [ImageProxy] Proxy created for photo1.jpg (no disk I/O yet)
  [ImageProxy] Proxy created for photo2.jpg (no disk I/O yet)
  [ImageProxy] Proxy created for photo3.jpg (no disk I/O yet)

=== User scrolls to first image ===
  [ImageProxy] First access -- loading real image now
  [HeavyImage] Loading photo1.jpg from disk... (expensive!)
  [HeavyImage] Displaying photo1.jpg (3840x2160)

=== User scrolls to first image again ===
  [HeavyImage] Displaying photo1.jpg (3840x2160)

=== Images 2 and 3 never viewed -- never loaded ===

4. Protection Proxy -- Access Control

The protection proxy checks permissions before forwarding the call.

// ============================================================
// PROTECTION PROXY -- ACCESS CONTROL
// ============================================================

// ---- The real object: a document store ----

class DocumentStore {
  constructor() {
    this.documents = new Map();
    this.documents.set('doc-1', {
      id: 'doc-1', title: 'Q4 Revenue Report', content: 'Revenue: $2.4M...',
      classification: 'confidential', owner: 'alice',
    });
    this.documents.set('doc-2', {
      id: 'doc-2', title: 'Team Lunch Menu', content: 'Pizza, salad...',
      classification: 'public', owner: 'bob',
    });
    this.documents.set('doc-3', {
      id: 'doc-3', title: 'Merger Plans', content: 'Acquiring XYZ Corp...',
      classification: 'top-secret', owner: 'ceo',
    });
  }

  getDocument(docId) {
    return this.documents.get(docId) || null;
  }

  updateDocument(docId, content) {
    const doc = this.documents.get(docId);
    if (doc) {
      doc.content = content;
      doc.lastModified = new Date().toISOString();
      console.log(`  [Store] Document ${docId} updated`);
    }
    return doc;
  }

  deleteDocument(docId) {
    const deleted = this.documents.delete(docId);
    console.log(`  [Store] Document ${docId} ${deleted ? 'deleted' : 'not found'}`);
    return deleted;
  }
}


// ---- Protection Proxy: enforces access control ----

class DocumentStoreProxy {
  constructor(store, currentUser) {
    this.store = store;
    this.currentUser = currentUser;
  }

  _checkReadAccess(doc) {
    if (!doc) return true; // no doc = no restriction

    const { role } = this.currentUser;

    if (doc.classification === 'public') return true;
    if (doc.classification === 'confidential' && ['manager', 'admin'].includes(role)) return true;
    if (doc.classification === 'top-secret' && role === 'admin') return true;
    if (doc.owner === this.currentUser.id) return true; // owners can always read

    return false;
  }

  _checkWriteAccess(doc) {
    if (!doc) return false;
    const { role, id } = this.currentUser;
    return doc.owner === id || role === 'admin';
  }

  getDocument(docId) {
    const doc = this.store.getDocument(docId);

    if (!doc) {
      console.log(`  [Proxy] Document ${docId} not found`);
      return null;
    }

    if (!this._checkReadAccess(doc)) {
      console.log(`  [Proxy] ACCESS DENIED: ${this.currentUser.id} (${this.currentUser.role}) cannot read "${doc.title}" (${doc.classification})`);
      throw new Error(`Access denied: insufficient permissions to read ${docId}`);
    }

    console.log(`  [Proxy] ACCESS GRANTED: ${this.currentUser.id} reading "${doc.title}"`);
    return doc;
  }

  updateDocument(docId, content) {
    const doc = this.store.getDocument(docId);

    if (!this._checkWriteAccess(doc)) {
      console.log(`  [Proxy] ACCESS DENIED: ${this.currentUser.id} cannot write to "${doc.title}"`);
      throw new Error(`Access denied: insufficient permissions to update ${docId}`);
    }

    console.log(`  [Proxy] ACCESS GRANTED: ${this.currentUser.id} updating "${doc.title}"`);
    return this.store.updateDocument(docId, content);
  }

  deleteDocument(docId) {
    if (this.currentUser.role !== 'admin') {
      console.log(`  [Proxy] ACCESS DENIED: only admins can delete documents`);
      throw new Error('Access denied: admin role required for deletion');
    }

    console.log(`  [Proxy] ACCESS GRANTED: admin ${this.currentUser.id} deleting ${docId}`);
    return this.store.deleteDocument(docId);
  }
}


// ---- Usage ----

const store = new DocumentStore();

// Regular employee tries to access documents
console.log('=== Employee access ===');
const employeeProxy = new DocumentStoreProxy(store, { id: 'dave', role: 'employee' });

try {
  employeeProxy.getDocument('doc-2'); // public -- OK
} catch (e) { console.log(`  Error: ${e.message}`); }

try {
  employeeProxy.getDocument('doc-1'); // confidential -- DENIED
} catch (e) { console.log(`  Error: ${e.message}`); }


// Manager access
console.log('\n=== Manager access ===');
const managerProxy = new DocumentStoreProxy(store, { id: 'alice', role: 'manager' });

try {
  managerProxy.getDocument('doc-1'); // confidential + owner -- OK
} catch (e) { console.log(`  Error: ${e.message}`); }

try {
  managerProxy.getDocument('doc-3'); // top-secret -- DENIED
} catch (e) { console.log(`  Error: ${e.message}`); }


// Admin access
console.log('\n=== Admin access ===');
const adminProxy = new DocumentStoreProxy(store, { id: 'superadmin', role: 'admin' });
adminProxy.getDocument('doc-3'); // top-secret -- OK for admin

5. Caching Proxy

The caching proxy stores results of expensive operations and returns cached data on subsequent calls.

// ============================================================
// CACHING PROXY
// ============================================================

class DatabaseService {
  async query(sql) {
    console.log(`  [DB] Executing query: ${sql}`);
    // Simulate expensive database call (200ms)
    await new Promise((resolve) => setTimeout(resolve, 200));
    return {
      rows: [
        { id: 1, name: 'Alice', email: 'alice@example.com' },
        { id: 2, name: 'Bob', email: 'bob@example.com' },
      ],
      executionTime: '200ms',
    };
  }
}


class CachingDatabaseProxy {
  constructor(db, options = {}) {
    this.db = db;
    this.cache = new Map();
    this.ttl = options.ttl || 60000;      // default: 60 seconds
    this.maxSize = options.maxSize || 100; // max cached queries
    this.stats = { hits: 0, misses: 0 };
  }

  async query(sql) {
    const cacheKey = sql.trim().toLowerCase();

    // Check cache
    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey);

      if (Date.now() - cached.timestamp < this.ttl) {
        this.stats.hits++;
        console.log(`  [Cache] HIT for: ${sql} (age: ${Date.now() - cached.timestamp}ms)`);
        return cached.data;
      }

      // Expired -- remove it
      this.cache.delete(cacheKey);
      console.log(`  [Cache] EXPIRED for: ${sql}`);
    }

    // Cache miss -- call real database
    this.stats.misses++;
    console.log(`  [Cache] MISS for: ${sql}`);
    const result = await this.db.query(sql);

    // Evict oldest if at capacity
    if (this.cache.size >= this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
      console.log(`  [Cache] Evicted oldest entry`);
    }

    this.cache.set(cacheKey, { data: result, timestamp: Date.now() });
    return result;
  }

  invalidate(sql) {
    const key = sql.trim().toLowerCase();
    this.cache.delete(key);
    console.log(`  [Cache] Invalidated: ${sql}`);
  }

  invalidateAll() {
    this.cache.clear();
    console.log(`  [Cache] All entries invalidated`);
  }

  getStats() {
    const total = this.stats.hits + this.stats.misses;
    return {
      ...this.stats,
      total,
      hitRate: total > 0 ? ((this.stats.hits / total) * 100).toFixed(1) + '%' : '0%',
      cacheSize: this.cache.size,
    };
  }
}


// ---- Usage ----

async function demo() {
  const db = new DatabaseService();
  const cachedDb = new CachingDatabaseProxy(db, { ttl: 5000 });

  console.log('=== First query (cache miss) ===');
  await cachedDb.query('SELECT * FROM users');

  console.log('\n=== Same query (cache hit) ===');
  await cachedDb.query('SELECT * FROM users');

  console.log('\n=== Different query (cache miss) ===');
  await cachedDb.query('SELECT * FROM orders');

  console.log('\n=== Cache stats ===');
  console.log(cachedDb.getStats());
  // { hits: 1, misses: 2, total: 3, hitRate: '33.3%', cacheSize: 2 }
}

demo();

6. Logging Proxy

The logging proxy records every operation for debugging, auditing, or analytics.

// ============================================================
// LOGGING PROXY
// ============================================================

class ApiClient {
  async get(url) {
    console.log(`  [API] GET ${url}`);
    return { status: 200, data: { message: 'success' } };
  }

  async post(url, body) {
    console.log(`  [API] POST ${url}`);
    return { status: 201, data: { id: 'new_123' } };
  }

  async put(url, body) {
    console.log(`  [API] PUT ${url}`);
    return { status: 200, data: { updated: true } };
  }

  async delete(url) {
    console.log(`  [API] DELETE ${url}`);
    return { status: 204 };
  }
}


class LoggingApiProxy {
  constructor(apiClient, logger = console) {
    this.api = apiClient;
    this.logger = logger;
    this.requestLog = [];
  }

  _createLogEntry(method, url, startTime, result, error = null) {
    const duration = Date.now() - startTime;
    const entry = {
      method,
      url,
      timestamp: new Date().toISOString(),
      duration: `${duration}ms`,
      status: error ? 'error' : result.status,
      error: error ? error.message : null,
    };
    this.requestLog.push(entry);
    return entry;
  }

  async get(url) {
    const start = Date.now();
    this.logger.log(`  [Log] --> GET ${url}`);
    try {
      const result = await this.api.get(url);
      const entry = this._createLogEntry('GET', url, start, result);
      this.logger.log(`  [Log] <-- GET ${url} [${entry.status}] (${entry.duration})`);
      return result;
    } catch (error) {
      this._createLogEntry('GET', url, start, null, error);
      this.logger.log(`  [Log] <-- GET ${url} [ERROR] ${error.message}`);
      throw error;
    }
  }

  async post(url, body) {
    const start = Date.now();
    this.logger.log(`  [Log] --> POST ${url}`, JSON.stringify(body).slice(0, 100));
    try {
      const result = await this.api.post(url, body);
      const entry = this._createLogEntry('POST', url, start, result);
      this.logger.log(`  [Log] <-- POST ${url} [${entry.status}] (${entry.duration})`);
      return result;
    } catch (error) {
      this._createLogEntry('POST', url, start, null, error);
      this.logger.log(`  [Log] <-- POST ${url} [ERROR] ${error.message}`);
      throw error;
    }
  }

  async put(url, body) {
    const start = Date.now();
    this.logger.log(`  [Log] --> PUT ${url}`);
    const result = await this.api.put(url, body);
    const entry = this._createLogEntry('PUT', url, start, result);
    this.logger.log(`  [Log] <-- PUT ${url} [${entry.status}] (${entry.duration})`);
    return result;
  }

  async delete(url) {
    const start = Date.now();
    this.logger.log(`  [Log] --> DELETE ${url}`);
    const result = await this.api.delete(url);
    const entry = this._createLogEntry('DELETE', url, start, result);
    this.logger.log(`  [Log] <-- DELETE ${url} [${entry.status}] (${entry.duration})`);
    return result;
  }

  getRequestLog() {
    return [...this.requestLog];
  }

  getStats() {
    const methods = {};
    for (const entry of this.requestLog) {
      methods[entry.method] = (methods[entry.method] || 0) + 1;
    }
    return {
      totalRequests: this.requestLog.length,
      byMethod: methods,
      errors: this.requestLog.filter((e) => e.error).length,
    };
  }
}


// ---- Usage: drop-in replacement ----

async function demo() {
  const api = new LoggingApiProxy(new ApiClient());

  await api.get('/api/users');
  await api.post('/api/users', { name: 'Alice' });
  await api.put('/api/users/1', { name: 'Alice Updated' });
  await api.delete('/api/users/2');

  console.log('\n=== Request Log ===');
  console.log(api.getStats());
}

demo();

7. JavaScript Proxy Object

JavaScript has a built-in Proxy class that makes creating proxies extremely convenient. It uses "traps" to intercept fundamental operations.

// ============================================================
// JAVASCRIPT BUILT-IN Proxy OBJECT
// ============================================================

// ---- Example 1: Validation Proxy ----

function createValidatedUser(user) {
  return new Proxy(user, {
    set(target, property, value) {
      // Validate before setting
      if (property === 'age') {
        if (typeof value !== 'number' || value < 0 || value > 150) {
          throw new TypeError(`Invalid age: ${value}`);
        }
      }

      if (property === 'email') {
        if (typeof value !== 'string' || !value.includes('@')) {
          throw new TypeError(`Invalid email: ${value}`);
        }
      }

      if (property === 'name') {
        if (typeof value !== 'string' || value.length < 2) {
          throw new TypeError(`Name must be at least 2 characters`);
        }
      }

      target[property] = value;
      console.log(`  Set ${property} = ${value}`);
      return true;
    },

    get(target, property) {
      if (property in target) {
        console.log(`  Get ${property} -> ${target[property]}`);
        return target[property];
      }
      console.log(`  Get ${property} -> undefined (property not found)`);
      return undefined;
    },
  });
}

console.log('=== Validation Proxy ===');
const user = createValidatedUser({ name: 'Alice', age: 30, email: 'alice@test.com' });
user.name = 'Bob';     // OK
user.age = 25;         // OK

try {
  user.age = -5;       // Throws TypeError
} catch (e) {
  console.log(`  Error: ${e.message}`);
}

try {
  user.email = 'bad';  // Throws TypeError
} catch (e) {
  console.log(`  Error: ${e.message}`);
}


// ---- Example 2: Read-Only Proxy (Freeze alternative) ----

function createReadOnly(obj) {
  return new Proxy(obj, {
    set(target, property, value) {
      throw new Error(`Cannot modify read-only property "${property}"`);
    },
    deleteProperty(target, property) {
      throw new Error(`Cannot delete read-only property "${property}"`);
    },
  });
}

console.log('\n=== Read-Only Proxy ===');
const config = createReadOnly({ apiUrl: 'https://api.example.com', timeout: 5000 });
console.log(`  apiUrl: ${config.apiUrl}`); // OK to read

try {
  config.apiUrl = 'hacked!'; // Throws Error
} catch (e) {
  console.log(`  Error: ${e.message}`);
}


// ---- Example 3: Auto-Logging Proxy (generic) ----

function createLoggingProxy(target, name = 'Object') {
  return new Proxy(target, {
    get(obj, prop) {
      const value = obj[prop];
      if (typeof value === 'function') {
        return function (...args) {
          console.log(`  [${name}] ${String(prop)}(${args.map(a => JSON.stringify(a)).join(', ')})`);
          const result = value.apply(obj, args);
          console.log(`  [${name}] ${String(prop)} -> ${JSON.stringify(result)}`);
          return result;
        };
      }
      return value;
    },
  });
}

console.log('\n=== Auto-Logging Proxy ===');
const calculator = createLoggingProxy({
  add(a, b) { return a + b; },
  multiply(a, b) { return a * b; },
}, 'Calculator');

calculator.add(3, 4);
calculator.multiply(5, 6);


// ---- Example 4: Negative Array Index Proxy ----

function createSmartArray(arr) {
  return new Proxy(arr, {
    get(target, prop) {
      const index = Number(prop);
      if (!isNaN(index) && index < 0) {
        // Support Python-style negative indexing
        return target[target.length + index];
      }
      return target[prop];
    },
  });
}

console.log('\n=== Negative Index Array ===');
const items = createSmartArray(['a', 'b', 'c', 'd', 'e']);
console.log(`  items[-1] = ${items[-1]}`); // 'e'
console.log(`  items[-2] = ${items[-2]}`); // 'd'
console.log(`  items[0]  = ${items[0]}`);  // 'a'

8. Combining Proxies

In practice, you often layer multiple proxy concerns:

// ============================================================
// COMBINING PROXIES: Caching + Logging + Protection
// ============================================================

class UserRepository {
  async findById(id) {
    console.log(`    [DB] SELECT * FROM users WHERE id = ${id}`);
    return { id, name: 'User ' + id, email: `user${id}@example.com` };
  }

  async update(id, data) {
    console.log(`    [DB] UPDATE users SET ... WHERE id = ${id}`);
    return { id, ...data, updated: true };
  }

  async delete(id) {
    console.log(`    [DB] DELETE FROM users WHERE id = ${id}`);
    return { deleted: true };
  }
}


// Factory that layers proxies
function createSecureUserRepository(currentUser) {
  const repo = new UserRepository();

  // Layer 1: Protection proxy
  const protectedRepo = {
    async findById(id) {
      return repo.findById(id); // everyone can read
    },
    async update(id, data) {
      if (currentUser.role !== 'admin' && currentUser.id !== id) {
        throw new Error('Access denied: can only update own profile');
      }
      return repo.update(id, data);
    },
    async delete(id) {
      if (currentUser.role !== 'admin') {
        throw new Error('Access denied: admin only');
      }
      return repo.delete(id);
    },
  };

  // Layer 2: Caching proxy (wraps protected)
  const cache = new Map();
  const cachedRepo = {
    async findById(id) {
      if (cache.has(id)) {
        console.log(`  [Cache] HIT user ${id}`);
        return cache.get(id);
      }
      console.log(`  [Cache] MISS user ${id}`);
      const user = await protectedRepo.findById(id);
      cache.set(id, user);
      return user;
    },
    async update(id, data) {
      cache.delete(id); // invalidate cache
      return protectedRepo.update(id, data);
    },
    async delete(id) {
      cache.delete(id); // invalidate cache
      return protectedRepo.delete(id);
    },
  };

  // Layer 3: Logging proxy (wraps cached)
  const loggedRepo = {
    async findById(id) {
      console.log(`[Log] findById(${id})`);
      const start = Date.now();
      const result = await cachedRepo.findById(id);
      console.log(`[Log] findById completed in ${Date.now() - start}ms`);
      return result;
    },
    async update(id, data) {
      console.log(`[Log] update(${id}, ${JSON.stringify(data)})`);
      return cachedRepo.update(id, data);
    },
    async delete(id) {
      console.log(`[Log] delete(${id})`);
      return cachedRepo.delete(id);
    },
  };

  return loggedRepo;
}


// ---- Usage ----

async function demo() {
  const userRepo = createSecureUserRepository({ id: '1', role: 'admin' });

  console.log('=== First fetch (cache miss) ===');
  await userRepo.findById('42');

  console.log('\n=== Second fetch (cache hit) ===');
  await userRepo.findById('42');

  console.log('\n=== Update (invalidates cache) ===');
  await userRepo.update('42', { name: 'Updated' });

  console.log('\n=== Fetch after update (cache miss again) ===');
  await userRepo.findById('42');
}

demo();
  Request flow with layered proxies:

  Client
    |
    v
  [Logging Proxy]  -- records the call
    |
    v
  [Caching Proxy]  -- returns cached result if available
    |
    v
  [Protection Proxy]  -- checks permissions
    |
    v
  [Real Repository]  -- hits the database

9. Before and After Comparison

Before (no proxy -- caching scattered everywhere)

// BAD: Caching logic duplicated across services
class UserService {
  constructor(db) {
    this.db = db;
    this.cache = new Map(); // caching inside business logic
  }

  async getUser(id) {
    // Cache check mixed with business logic
    if (this.cache.has(id)) {
      return this.cache.get(id);
    }
    const user = await this.db.query(`SELECT * FROM users WHERE id = ${id}`);

    // Permission check mixed with business logic
    if (!this.currentUser.canViewUser(user)) {
      throw new Error('Forbidden');
    }

    // Logging mixed with business logic
    console.log(`User ${id} fetched by ${this.currentUser.id}`);

    this.cache.set(id, user);
    return user;
  }
}
// Problem: UserService does caching + auth + logging + business logic
// Problem: Cannot reuse caching logic for OrderService
// Problem: Cannot disable logging without editing code

After (proxy pattern -- clean separation)

// GOOD: Each concern in its own proxy layer
class UserService {
  constructor(userRepo) {
    this.userRepo = userRepo; // could be real, cached, logged, or protected
  }

  async getUser(id) {
    return this.userRepo.findById(id); // just business logic
  }
}

// Compose the layers you need:
const repo = new UserRepository();
const cached = new CachingProxy(repo);
const logged = new LoggingProxy(cached);
const protected_ = new ProtectionProxy(logged, currentUser);

const service = new UserService(protected_);

10. When to Use and When to Avoid

Use Proxy when:

ScenarioProxy type
Expensive objects that may not be neededVirtual proxy (lazy loading)
Access control / permission checksProtection proxy
Avoiding repeated expensive computationsCaching proxy
Audit trail / debuggingLogging proxy
Accessing remote services transparentlyRemote proxy
Input validationValidation proxy (JS Proxy)

Avoid Proxy when:

ScenarioWhy Proxy is wrong
The overhead of the proxy layer is unacceptableAdds indirection (usually negligible)
Simple direct access is sufficientUnnecessary complexity
You need to add new behavior (not just control)Use Decorator instead
The interface would changeUse Adapter instead

11. Key Takeaways

  1. A Proxy has the same interface as the real object -- the client cannot tell the difference.
  2. There are multiple proxy types: virtual (lazy loading), protection (access control), caching, logging, and remote.
  3. JavaScript's built-in Proxy class provides powerful metaprogramming capabilities: you can trap get, set, deleteProperty, apply, construct, and more.
  4. Proxies can be layered -- logging wraps caching, which wraps protection, which wraps the real object.
  5. The key difference from Decorator: a proxy controls access to an existing object, while a decorator adds new behavior.
  6. Real-world uses: ORM lazy loading, API rate limiting, CDN caching, authentication middleware, React's Proxy-based state management.

12. Explain-It Challenge

Without looking back, explain in your own words:

  1. Name the four types of proxies and give a one-sentence use case for each.
  2. How does a virtual proxy save memory in an image gallery application?
  3. What is the difference between a Proxy and a Decorator?
  4. How does JavaScript's built-in Proxy object differ from the classic OOP proxy pattern?
  5. Draw an ASCII diagram showing three layered proxies (logging -> caching -> protection -> real object).

Navigation: <- 9.4.b -- Facade | 9.4.d -- Decorator ->