Skip to main content

What is Proxy design pattern?

Proxy is a structural design pattern where a surrogate object sits in front of a real resource and controls access to it, adding behavior like lazy loading, validation, or caching without changing the resource's interface.

Theory

TL;DR

  • Hotel front desk analogy: you talk to the proxy, it contacts the real object only when needed, and checks credentials along the way
  • Same interface as the real object, but every call passes through the proxy first
  • JavaScript has native Proxy (ES6+) with traps for get, set, apply, and more
  • Use when the target is expensive to create, needs access control, or requires call tracking
  • Skip when there is no extra logic to add - direct access is simpler

Quick Example

javascript
const target = { name: 'John', age: 25 }; const handler = { get(target, prop) { if (prop === 'age' && target[prop] < 0) { throw new Error('Invalid age'); } return target[prop]; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // "John" console.log(proxy.age); // 25 target.age = -5; console.log(proxy.age); // Error: Invalid age

The proxy intercepts every property read. The original target is untouched - proxy.age threw, but target.age is still -5.

Key Difference

Proxy provides the exact same interface as the real object. Client code sees nothing different. Every operation routes through the proxy first, and that is where the extra logic lives. This separates access concerns from the core object.

Proxy and Decorator both wrap objects, but the intent differs. Proxy controls access to one specific target: lazy creation, caching, authorization. Decorator adds behavior by stacking wrappers, often on many objects at once. Same structure, different goals.

When to Use

  • Expensive resource (DB query, image load): proxy for lazy loading or caching so the real work happens once
  • Access control (admin-only fields, API keys): proxy to check permissions before returning data
  • Remote or heavy object: proxy as a lightweight local stand-in
  • Rate limiting: proxy to throttle outgoing API calls
  • Call tracking: proxy to log reads and writes without modifying the original

Avoid Proxy when there is no extra behavior to add. The overhead is small but measurable.

How JavaScript Proxy Works Internally

JavaScript's Proxy (ES6+) uses V8's meta-programming hooks to intercept operations. When you read proxy.prop, V8 checks the get trap in your handler, runs your code, then delegates to the target if you return normally. Traps like set and apply hook into property writes and function calls. Node.js and browsers follow the same spec.

Reflect is the safe way to delegate inside a trap. Use Reflect.get(target, prop) instead of target[prop] to avoid accidental recursion when the target is itself a proxy.

Common Mistakes

1. Mutating the target directly bypasses the proxy.

javascript
const proxy = new Proxy( { count: 0 }, { set(t, p, v) { console.log('Set!'); t[p] = v; return true; } } ); proxy.count++; // Logs "Set!" target.count++; // Nothing logged - proxy bypassed

The proxy only intercepts access through itself. In practice, every time a team exposes both the proxy and the raw target, the proxy stops being useful within days. Expose only the proxy, never the raw target.

2. Forgetting return true in the set trap.

javascript
const handler = { set(t, p, v) { t[p] = v; } // Missing return true }; const p = new Proxy({}, handler); p.x = 1; // Silent failure in non-strict, TypeError in strict mode

The set trap must return true per the spec. Always add return true after the assignment.

3. Infinite recursion inside traps.

javascript
const handler = { get(t, p) { return t[p]; } // Loops forever if t is also a Proxy };

Use Reflect.get(target, prop) to avoid re-triggering the same trap.

4. Assuming all operations are trappable.

There is no delete trap by default. delete proxy.x throws a TypeError in strict mode unless you add a deleteProperty trap. Check what traps exist before assuming an operation is intercepted.

Real-World Usage

  • Vue 3: proxies detect deep property changes and trigger re-renders automatically
  • Apollo Client: field-level caching that proxies GraphQL resolvers
  • React DevTools: proxies component state for inspection without mutation
  • Express caching layers: proxy wrappers around DB calls for lazy loading
  • Proxyquire: mocking module dependencies in Node.js tests

Follow-up Questions

Q: What is the difference between Proxy and Decorator patterns?
A: Proxy controls access to one specific target (lazy loading, caching, authorization). Decorator adds new behavior by stacking wrappers and applies to many objects. The structure looks similar, but the intent is different.

Q: Implement a logging proxy for any function.
A:

javascript
const loggingProxy = (fn) => new Proxy(fn, { apply(target, thisArg, args) { console.log(`Calling ${target.name} with`, args); return target.apply(thisArg, args); } });

Q: How does Proxy perform vs direct access?
A: Minor overhead, around 1-5% in V8 benchmarks. For I/O-bound operations that gap disappears. Avoid Proxy in hot inner loops where the overhead accumulates.

Q: What is the difference between virtual proxy and protection proxy?
A: Virtual proxy delays expensive object creation until it is actually needed, for example lazy-loading a large image. Protection proxy restricts access based on permissions, like checking admin rights before returning sensitive data.

Q: In a Node.js cluster, how do you sync proxy state across processes?
A: You can't directly. Proxies are in-memory objects and don't serialize across process boundaries. For shared cache, use Redis or a message-passing system instead.

Examples

Basic: Native Proxy with Validation

javascript
const user = { name: 'John', age: 25 }; const handler = { get(target, prop) { if (prop === 'age' && target[prop] < 0) { throw new Error('Invalid age'); } return target[prop]; }, set(target, prop, value) { if (prop === 'age' && typeof value !== 'number') { throw new TypeError('Age must be a number'); } target[prop] = value; return true; // required by the Proxy spec } }; const proxy = new Proxy(user, handler); console.log(proxy.name); // "John" proxy.age = 31; // works fine proxy.age = 'thirty'; // TypeError: Age must be a number

Both get and set traps are active. Reading proxy.name goes through get, setting proxy.age to a string fails at set. The original user object is never modified directly.

Intermediate: Caching Proxy for API Calls

javascript
class ApiProxy { constructor(api) { this.api = api; this.cache = new Map(); } async getUser(id) { if (!this.cache.has(id)) { console.log('Fetching from API...'); this.cache.set(id, await this.api.fetchUser(id)); } return this.cache.get(id); } } const realApi = { async fetchUser(id) { return { id, name: 'User' + id }; } }; const proxy = new ApiProxy(realApi); console.log(await proxy.getUser(1)); // Fetching from API... // { id: 1, name: 'User1' } console.log(await proxy.getUser(1)); // { id: 1, name: 'User1' } (from cache, no API call)

The real API is called once per id. After that, the proxy serves from its internal Map. Apollo Client uses a similar pattern for GraphQL query caching.

Advanced: Function Logging Proxy

javascript
const loggingProxy = (fn) => new Proxy(fn, { apply(target, thisArg, args) { console.log(`Calling ${target.name} with`, args); const result = target.apply(thisArg, args); console.log('Result:', result); return result; } }); function calculateTotal(price, tax) { return price + price * tax; } const logged = loggingProxy(calculateTotal); logged(100, 0.2); // Calling calculateTotal with [100, 0.2] // Result: 120

The apply trap fires on every function call. This gives you logging, timing, or argument validation without touching calculateTotal itself. The same pattern works for any function.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?