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?
- 2. Types of Proxies
- 3. Virtual Proxy -- Lazy Loading
- 4. Protection Proxy -- Access Control
- 5. Caching Proxy
- 6. Logging Proxy
- 7. JavaScript Proxy Object
- 8. Combining Proxies
- 9. Before and After Comparison
- 10. When to Use and When to Avoid
- 11. Key Takeaways
- 12. Explain-It Challenge
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:
| Pattern | Same interface? | What it does |
|---|---|---|
| Proxy | Yes | Controls access to the same object |
| Adapter | No (translates) | Changes the interface |
| Decorator | Yes | Adds new behavior |
| Facade | No (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:
| Scenario | Proxy type |
|---|---|
| Expensive objects that may not be needed | Virtual proxy (lazy loading) |
| Access control / permission checks | Protection proxy |
| Avoiding repeated expensive computations | Caching proxy |
| Audit trail / debugging | Logging proxy |
| Accessing remote services transparently | Remote proxy |
| Input validation | Validation proxy (JS Proxy) |
Avoid Proxy when:
| Scenario | Why Proxy is wrong |
|---|---|
| The overhead of the proxy layer is unacceptable | Adds indirection (usually negligible) |
| Simple direct access is sufficient | Unnecessary complexity |
| You need to add new behavior (not just control) | Use Decorator instead |
| The interface would change | Use Adapter instead |
11. Key Takeaways
- A Proxy has the same interface as the real object -- the client cannot tell the difference.
- There are multiple proxy types: virtual (lazy loading), protection (access control), caching, logging, and remote.
- JavaScript's built-in
Proxyclass provides powerful metaprogramming capabilities: you can trapget,set,deleteProperty,apply,construct, and more. - Proxies can be layered -- logging wraps caching, which wraps protection, which wraps the real object.
- The key difference from Decorator: a proxy controls access to an existing object, while a decorator adds new behavior.
- 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:
- Name the four types of proxies and give a one-sentence use case for each.
- How does a virtual proxy save memory in an image gallery application?
- What is the difference between a Proxy and a Decorator?
- How does JavaScript's built-in
Proxyobject differ from the classic OOP proxy pattern? - Draw an ASCII diagram showing three layered proxies (logging -> caching -> protection -> real object).
Navigation: <- 9.4.b -- Facade | 9.4.d -- Decorator ->