skip to content

questions

6

What is the Proxy design pattern, and what problem does it solve?

level: juniorimportance: must knowfreq 72%

answer

  1. Surrogate that controls access
  2. Same interface as real subject
  3. Virtual / protection / remote / smart reference
  4. Lazy creation, permission check, network hop
  5. Controls access ≠ adds behavior

basics

~20 s

Proxy is a stand-in object that implements the same interface as a real object and forwards calls to it. Because callers go through the proxy, it can control access — delay creation, check permissions, cache, or talk over a network — without the caller knowing.

solid answer

~50 s

Proxy provides a surrogate or placeholder for another object in order to control access to it. The proxy implements the same abstraction (interface/abstract type) as the real subject, holds a reference to it, and forwards requests — adding access-control logic before, after, or instead of forwarding. Classic variants: a virtual proxy defers creating an expensive real subject until a method is actually called; a protection proxy checks the caller's rights before forwarding; a remote proxy represents an object living in another process or machine and hides the marshalling/network call; a smart reference adds bookkeeping such as reference counting, caching, or lazy locking. The key structural property is substitutability: because proxy and real subject share the interface, client code is unchanged. Proxy differs from Decorator in intent — Proxy controls whether/when you reach the subject, Decorator adds behavior to a request that always reaches it.

code

typescript · 15 lines
typescript
interface Report { data(): string }

class HeavyReport implements Report {
  constructor(private id: string) { /* expensive build */ }
  data() { return "..." }
}

class LazyReport implements Report {          // virtual proxy
  private real: Report | null = null
  constructor(private id: string) {}
  data() {
    this.real ??= new HeavyReport(this.id)    // created on first use only
    return this.real.data()
  }
}

go deeper

for a junior

State the intent in one line — a stand-in with the same interface that controls access — and give one concrete case, usually lazy loading of something expensive.

for a middle

Name the four variants (virtual, protection, remote, smart reference) with an example each, and sketch the Subject/RealSubject/Proxy structure.

for a senior

Add the failure modes: identity breakage, deferred construction errors, thread-safe lazy init, and the fact that transparency is a feature locally but a hazard remotely.

for a principal

Frame Proxy as a general interception seam — the same idea scales from an in-process object to sidecars and API gateways — and discuss when the indirection is worth its debugging and operational cost.

## The problem Sometimes you cannot, or should not, let a caller hold a direct reference to an object: - The object is **expensive to create** (loads a 40 MB image, opens a database connection, builds a large in-memory index) and might never be used. - The object **must only be reachable by authorized callers**. - The object **lives somewhere else** — another process, another machine — so the call must be serialized, sent over a network, and the reply decoded. - You want to **add bookkeeping** around every access: caching results, counting references, logging, opening a lock. In all four cases the caller's code should not have to change. That is what Proxy buys you. ## The structure Three participants: 1. **Subject** — the abstraction (an interface or abstract type) that declares the operations. Example: `Image { render() }`. 2. **RealSubject** — the actual implementation that does the work. Example: `BitmapImage`. 3. **Proxy** — implements *the same Subject interface*, holds a reference (or enough information to obtain one, e.g. a filename or a remote address) to the RealSubject, and forwards calls to it, wrapping the forwarding in access-control logic. Because `Proxy` and `RealSubject` are both `Subject`, the client is written against `Subject` and cannot tell them apart. This substitutability is the whole trick — it is why the pattern is *transparent*. ## The four canonical variants | Variant | What it controls | Typical logic in the proxy | |---|---|---| | **Virtual proxy** | *When* the real object is created | On first call: create RealSubject, cache it, then forward. Later calls forward directly. Also used for lazy-loading collections and placeholder images. | | **Protection (access) proxy** | *Who* may call | Check caller identity/role/permission; forward or throw/deny. | | **Remote proxy** (a.k.a. *ambassador*, *stub*) | *Where* the object is | Serialize arguments, send over the wire, wait for reply, deserialize result, translate transport errors into domain errors. | | **Smart reference / smart proxy** | *What happens around* each access | Reference counting, caching results, lazy locking, metering, audit logging, retry. | A fifth commonly cited one is the **caching proxy** (arguably a smart reference) — memoize the result of expensive reads and serve subsequent reads without touching the real subject. ## A minimal virtual proxy, step by step ``` interface Image { render(): void } class BitmapImage implements Image { // RealSubject constructor(path) { this.pixels = loadFromDisk(path) } // expensive render() { draw(this.pixels) } } class ImageProxy implements Image { // Proxy constructor(path) { this.path = path; this.real = null } render() { if (this.real == null) this.real = new BitmapImage(this.path) // lazy this.real.render() } } ``` A document holding 500 `ImageProxy` objects opens instantly; only images actually scrolled into view pay the load cost. ## Trade-offs and edge cases - **Indirection cost** — one extra call hop per operation; usually negligible, but it matters in hot loops, and it complicates stack traces and debugging. - **Object identity** — `proxy != realSubject`. Reference equality checks, identity-keyed maps, `instanceof`-style downcasts to the concrete class, and reflection over the concrete type can all break. Well-behaved proxies forward `equals`/`hashCode`/`toString` semantics where the language allows. - **Deferred failure** — a virtual proxy moves construction errors from creation time to first-use time, often deep inside unrelated code. Decide deliberately whether that is acceptable. - **Concurrency** — the naive `if (real == null) real = create()` is a race: two threads can both create the real subject. Use double-checked locking with a volatile/atomic field, a lazy holder, or a synchronized initializer. - **Leaky abstraction for remote proxies** — a remote call can be slow, can time out, can partially fail. Making it look exactly like a local call is the classic "fallacies of distributed computing" trap; expose timeouts/retries/failure types rather than pretending they don't exist. - **Self-invocation** — when a framework generates the proxy (rather than you writing it), a call the real object makes to *its own* method does **not** go through the proxy, so the proxy's logic is silently skipped. ## Where you have already met it ORM lazy-loaded associations, framework-generated transaction/security/caching interceptors, RPC and gRPC client stubs, virtual-DOM/lazy-image placeholders, HTTP forward and reverse proxies, service-mesh sidecars, and mocking libraries that hand you a stand-in object implementing a real interface — all are proxies in the pattern sense.

  • If the proxy just forwards every call unchanged, what has it actually bought you?
    Nothing yet — a pure pass-through proxy is dead weight. The value comes from the logic wrapped around the forwarding (deferred creation, an authorization check, a network hop, a cache). If there is no such logic and none is planned, remove the proxy.
  • Does the client have to know it is holding a proxy?
    No, and normally it must not — that transparency is the point. The exception is remote proxies, where hiding latency and partial failure is dangerous; there the failure modes should be visible in the contract even if the call syntax stays the same.

A building's front desk. It presents the same 'talk to the company' interface as the person you want, but it decides whether to let you through (protection), doesn't wake the executive until someone actually asks for them (virtual), and can phone a colleague in another city and relay the answer (remote).

context

open as a page

How does the Proxy pattern differ from the Decorator pattern, given that both wrap an object behind the same interface?

level: middleimportance: must knowfreq 68%

basics

~20 s

Their structures look alike, but the intent differs. Proxy controls access to the object — whether, when, and from where you reach it — and usually creates or owns it. Decorator adds extra behavior to a request that always reaches the wrapped object, and is composed from outside.

open as a page

You implement a virtual proxy that creates an expensive object on first use. What correctness pitfalls must you handle?

level: middleimportance: should knowfreq 52%

basics

~20 s

Make the lazy creation thread-safe (two threads can both see "not created yet"), decide what happens if creation fails on first call, and remember the proxy is a different object from the real one — identity checks, equality, and type casts on the concrete class can break.

open as a page

Frameworks generate proxies at runtime to add transactions, security, or caching. How does that generation work, and what are its documented limitations?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The framework returns a generated stand-in instead of your object: either a class implementing your interfaces, or a subclass of your class, that runs extra logic then delegates. Limitations: only calls that go through the returned reference are intercepted, so internal self-calls, final/private/static methods, and direct field access are not.

open as a page

Compare a protection proxy and a remote proxy: what does each control, and what specific hazards does each introduce?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A protection proxy controls who may call — it checks the caller's rights and forwards or denies. A remote proxy controls where the object is — it hides that the real object lives in another process or machine. The first can be bypassed if callers reach the subject directly; the second hides latency and partial failure.

open as a page

Beyond a single class, where does the Proxy pattern show up at the architecture level, and when is adding a proxy the wrong call?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

The same idea scales up: API gateways, reverse proxies, service-mesh sidecars, caching layers, and database connection proxies all stand in front of something and control access to it. It's the wrong call when it adds a hop and an operating cost without policy — pure pass-through indirection.

open as a page