designpattern.site

Proxy Pattern

Provide a surrogate or placeholder for another object to control, enhance, or defer access to it.

Overview

You do not always want code to talk directly to an object. The object might be expensive to create, located across a network, require permission checks before use, or benefit from transparent caching. The Proxy pattern solves this by inserting a stand-in object — the proxy — that implements the same interface as the real object and adds behavior around every call.

From the caller's perspective, nothing changes. It calls the same methods on the same interface. The proxy quietly intercepts each call and decides whether to forward it, answer it from a cache, block it, or log it before passing it along.

This is the same principle your computer uses when it routes outbound traffic through a network proxy: you do not change how you open a URL, but the proxy sits in between and can filter, cache, or rewrite the request.

Problem & Motivation

Imagine you are building a dashboard that displays data from a third-party weather API. The API charges per request and has a rate limit. Several components on the page each call weatherService.getForecast(city) independently, potentially sending duplicate requests within the same render cycle.

Without a proxy, your options are unattractive:

  • Scatter cache-check logic across every component that calls the service
  • Wrap the service in a one-off helper class that only half the team knows about
  • Accept the duplicate charges and hope the rate limit is never hit

A caching proxy wraps the real service, holds the same interface, stores responses keyed by city, and returns cached results for repeated calls — all without the calling components knowing or caring. The real API is called exactly once per city per cache window.

Class Diagram

Loading diagram...

The structural points to notice:

  • Subject is the shared interface — both RealSubject and Proxy implement it.
  • Proxy holds a reference to RealSubject and forwards calls after performing its extra work.
  • Client is coded to the Subject interface and never knows whether it holds a proxy or the real object.
  • The proxy can be swapped in at construction time without touching any calling code.

Implementation

The examples below model a WeatherService proxy that caches API responses for 60 seconds. The real service simulates a network call with a console log. The proxy short-circuits repeat calls within the cache window.

interface WeatherService {
  getForecast(city: string): string;
}

// Real service — simulates an expensive API call
class RealWeatherService implements WeatherService {
  getForecast(city: string): string {
    console.log(`[API] Fetching forecast for ${city}`);
    return `Sunny, 24°C in ${city}`;
  }
}

interface CacheEntry {
  result: string;
  expiresAt: number;
}

// Caching proxy — same interface, adds transparent caching
class CachingWeatherProxy implements WeatherService {
  private readonly real: WeatherService;
  private readonly cache = new Map<string, CacheEntry>();
  private readonly ttlMs: number;

  constructor(real: WeatherService, ttlMs = 60_000) {
    this.real = real;
    this.ttlMs = ttlMs;
  }

  getForecast(city: string): string {
    const entry = this.cache.get(city);
    if (entry && Date.now() < entry.expiresAt) {
      console.log(`[Cache] Returning cached forecast for ${city}`);
      return entry.result;
    }

    const result = this.real.getForecast(city);
    this.cache.set(city, { result, expiresAt: Date.now() + this.ttlMs });
    return result;
  }
}

// Client code — depends only on the WeatherService interface
function displayDashboard(service: WeatherService): void {
  console.log(service.getForecast("London")); // hits API
  console.log(service.getForecast("London")); // served from cache
  console.log(service.getForecast("Tokyo"));  // hits API
}

const proxy = new CachingWeatherProxy(new RealWeatherService());
displayDashboard(proxy);
// [API] Fetching forecast for London
// Sunny, 24°C in London
// [Cache] Returning cached forecast for London
// Sunny, 24°C in London
// [API] Fetching forecast for Tokyo
// Sunny, 24°C in Tokyo

Four Flavors of Proxy

The caching proxy above is one of several common proxy types, each intercepting calls for a different reason:

  • Virtual proxy — defers creation of a heavyweight object until it is actually needed (lazy initialization). A document editor might show a placeholder for a high-resolution image and only decode the full bitmap when the user scrolls to it.
  • Protection proxy — checks permissions before forwarding a call. An AdminServiceProxy might inspect a user's role and throw an UnauthorizedError before the real service ever runs.
  • Caching proxy — stores results of expensive calls and returns them for repeated identical requests, as shown above.
  • Logging / monitoring proxy — records call timing, arguments, and outcomes without modifying production code. This is the core technique behind many APM agents and ORM query loggers.

The structural code is identical for all four types — only the interception logic differs.

Real-World Examples

ORM lazy loading — Hibernate and SQLAlchemy both return proxy objects when you load an entity with associations. The related collection (e.g., order.items) appears to be a plain list, but the proxy intercepts the first access, fires a SQL query, and hydrates the collection on demand. You never write any lazy-loading code yourself; the proxy does it transparently.

Service meshes and API gateways — Envoy, Nginx, and AWS API Gateway are network-level proxies. Each microservice call passes through a sidecar proxy that handles retries, circuit breaking, TLS termination, and metrics. The service itself is unaware of all this; it just receives and sends plain HTTP.

JavaScript Proxy object — The language has a built-in Proxy constructor that intercepts property reads, writes, function calls, and more via traps. State management libraries like MobX use it to track which observables a component reads so they can schedule precise re-renders.

CDN edge caching — A CDN node is a large-scale caching proxy. When you request cdn.example.com/image.png, the edge server checks its local cache. On a miss it fetches from your origin server, stores the response, and returns it. Subsequent visitors at the same edge location are served from cache without touching your origin.

Pros and Cons

Advantages

Transparent to callers — because the proxy and real object share an interface, client code does not change when a proxy is introduced or removed.

Open/Closed Principle — cross-cutting concerns like caching, logging, and access control are added in the proxy without modifying the real subject class.

Deferred cost — virtual proxies let you pay the creation or loading cost of expensive objects only when (and if) they are actually needed.

Fine-grained access control — protection proxies can enforce per-method or per-role permissions in one place, rather than scattered across every call site.

Testability — proxy classes are easy to stub or spy on in tests because they depend on the subject interface, not the concrete implementation.

Disadvantages

Indirection overhead — every call goes through an extra object. For hot paths in performance-critical code, even a thin proxy adds measurable overhead.

Increased complexity — introducing a proxy layer adds a class and a concept. Readers must understand that the object they hold might not be the real one.

Latent bugs from transparency — the very feature that makes proxies convenient (callers do not know) can hide bugs. A caching proxy that returns stale data is invisible until the mismatch causes a failure.

Interface coupling — the proxy must implement every method of the subject interface. When the interface grows, all proxy implementations must be updated in lockstep.

Debugging difficulty — stack traces through proxy layers are harder to read, and behavior differences between the proxy and the real object can be confusing under a debugger.

When to Use / When to Avoid

Use Proxy when:

  • You need lazy initialization for an object that is expensive to create but may never be used.
  • You need access control without modifying the real subject (protection proxy).
  • You want to cache results of expensive or rate-limited operations transparently (caching proxy).
  • You need to log, trace, or measure method calls across a class boundary without touching the class itself (logging proxy).
  • You are working with a remote object and want local code to look and feel like a local call (remote proxy / RPC stub).

Avoid Proxy when:

  • The real subject is cheap to create and the proxy adds complexity without proportional benefit.
  • You only need the extra behavior in one place — a simple wrapper function or local variable may be cleaner.
  • The caching or access logic is complex enough that hiding it behind a transparent interface will confuse maintainers.
  • The subject interface is very large and will change frequently — keeping all proxy implementations in sync becomes a maintenance burden.
  • Performance is critical and every extra method call matters — profile first, then decide whether the indirection is acceptable.

Decorator — the most commonly confused sibling. Both Decorator and Proxy wrap an object behind the same interface. The distinction is intent: a Decorator adds new behavior (e.g., adding scroll bars to a text view), while a Proxy controls access to the existing behavior. Decorators are typically stacked in chains; proxies usually sit as a single transparent layer.

Adapter — converts one interface to another. A Proxy always preserves the subject's interface; an Adapter intentionally changes it. If you find yourself renaming or combining methods, you are likely writing an Adapter.

Facade — simplifies a complex subsystem behind a single entry point. A Facade does not implement the subsystem's interface; it defines its own simpler one. Proxies and Facades both provide an indirection layer, but for very different reasons.

Flyweight — reduces memory by sharing common state across many objects. A virtual proxy is occasionally combined with Flyweight: the proxy defers creation, and if the real object can be shared, the proxy hands back a shared Flyweight instance.

Design Tip

When you find yourself writing the same cache-check, permission-check, or log statement in several places before calling a service, that repetition is a signal to reach for a proxy. Extract the cross-cutting concern into a proxy class, swap it in at the construction site, and delete the scattered boilerplate. The real object stays focused on its core responsibility, and the proxy handles the plumbing consistently in one place.

On this page