Quick Answer

A Proxy is an object that implements the exact same interface as a real object, sits in front of it, and controls access to it — adding caching, lazy loading, or access checks without the caller knowing anything changed. The kind of proxy most developers have heard of, a reverse proxy like Nginx, is just one real-world instance of this same idea applied to network requests instead of in-process objects.

What a Proxy Actually Is

A Proxy is an object that implements the same interface as some "real" object (the pattern calls it the subject), and stands in for it. Anyone holding a reference to the proxy calls the exact same methods they'd call on the real thing — the proxy's entire job is to intercept those calls and decide what to do: forward them unchanged, forward them with something extra attached, or refuse to forward them at all.

The defining property, and the thing that separates a proxy from just "a wrapper class," is that the caller can't tell the difference. If swapping the real object for its proxy requires the caller to change how it calls anything, it's not a transparent proxy anymore — it's just a different object with a similar name. That transparency is what makes the pattern useful: you can insert a proxy into an existing codebase at the one place objects get constructed, and every existing call site keeps working exactly as before, now with whatever the proxy adds.

The classic Gang of Four book splits this into a few named flavors based on what the proxy adds — virtual (defers expensive creation), protection (adds access control), remote (hides that the real object lives on another machine), and caching/smart-reference (adds bookkeeping like caching or reference counting) — but all four share the identical structural shape: same interface, one layer of indirection, one decision made before delegating.

Virtual Proxy: Don't Build It Until Someone Asks

A virtual proxy delays creating an expensive object until it's actually needed, while letting code hold a reference to "it" the whole time as if it already existed. The textbook example is a document editor that shows a placeholder icon for a large embedded image and only decodes and loads the full image data the moment it actually needs to be painted on screen — every part of the document model can hold a reference to "the image" from the moment the document opens, without the cost of loading every image up front.

This shows up constantly in ORMs under the name "lazy loading": calling order.customer on an ORM model often returns a proxy object immediately, and only fires the actual database query the first time you access a field on it, like order.customer.name. The calling code never had to know it was holding a proxy instead of a real, already-loaded Customer row — which is also exactly why lazy loading causes the N+1 query problem when you loop over many orders and access .customer.name on each: every iteration silently triggers its own proxy-initiated query.

Caching Proxy: Measured, Not Estimated

A caching proxy is the easiest flavor to demonstrate honestly, because the effect is directly measurable. Here's a real "expensive" service (it just sleeps to simulate genuine latency, like a slow external API or a heavy aggregation query) and a proxy in front of it that caches by argument:

class ExpensiveReportService:
    def generate_report(self, region: str) -> str:
        time.sleep(0.4)  # simulate real latency
        return f"Sales report for {region}: total = 842,300 INR"


class CachingReportProxy:
    def __init__(self, real_service: ExpensiveReportService):
        self._real_service = real_service
        self._cache = {}

    def generate_report(self, region: str) -> str:
        if region in self._cache:
            return self._cache[region]
        result = self._real_service.generate_report(region)
        self._cache[region] = result
        return result

CachingReportProxy exposes the exact same generate_report(region) method as the real service — callers construct it once and never need to know whether a given call actually hit the slow path. Timing four calls (two distinct regions, each called twice) with time.perf_counter() around each call produced this actual, unedited output:

First call (cache miss): 400.208 ms -> Sales report for Karnataka: total = 842,300 INR
Second call (cache hit): 0.00260 ms -> Sales report for Karnataka: total = 842,300 INR
Third call, different key (cache miss): 400.514 ms -> Sales report for Maharashtra: total = 842,300 INR
Fourth call, same as third (cache hit): 0.00260 ms -> Sales report for Maharashtra: total = 842,300 INR

Speedup on repeated call: roughly 153,939x faster

That six-figure speedup number is real for this exact demo but shouldn't be quoted as a general fact about caching — it's the ratio between a deliberately slow 400ms simulated call and a dictionary lookup, which is about as favorable a comparison as caching can get. The honest takeaway isn't the specific multiplier; it's the shape of the result: the first call per unique key pays the real cost, and that is the only caller-visible difference — nothing about how generate_report() is called changes.

Two things a caching proxy has to get right that this minimal version glosses over: cache invalidation (this demo's cache never expires or clears, which is fine for a demo and wrong for anything where the underlying data changes) and cache key correctness (caching by the wrong argument, or forgetting an argument that affects the result, silently serves stale or wrong data to callers who have no way to know the proxy lied to them).

Protection Proxy: Checking Before Delegating

A protection proxy adds an access check in front of the real object's methods, refusing to delegate the call at all if the check fails. This shows up in ORMs and API clients as row-level or field-level permission checks — a repository or query object that looks identical whether you're an admin or a regular user, but which silently filters or refuses certain operations based on who's calling:

class ProtectedDocumentStore:
    def __init__(self, real_store, current_user):
        self._real_store = real_store
        self._user = current_user

    def delete(self, doc_id: int) -> None:
        doc = self._real_store.get(doc_id)
        if doc.owner_id != self._user.id and not self._user.is_admin:
            raise PermissionError(f"User {self._user.id} cannot delete doc {doc_id}")
        self._real_store.delete(doc_id)

The value here isn't just "fewer if-statements in the caller" — it's that the permission check lives in exactly one place regardless of how many call sites eventually call .delete(). Every new caller automatically gets the check for free, instead of every new caller needing to remember to add it themselves before calling the real store directly.

Remote Proxy: The Idea Behind Reverse Proxies

A remote proxy stands in locally for an object that actually lives somewhere else over the network, hiding the fact that calling a method now means serialization and a network round trip. This is the conceptual backbone of RPC stubs, gRPC-generated client code, and any "client library" that lets you write api.getUser(id) as if it were a local function call when it's really an HTTP request underneath.

A reverse proxy like Nginx or a CDN edge node is the same structural idea, one level up the stack: instead of one object standing in for another object, it's one server standing in for a group of backend servers, so every client that connects sees a single, consistent address regardless of what's actually running behind it — load-balanced across replicas, cached at the edge, or serving from a completely different service for one specific URL path. If you've read this site's Nginx reverse proxy guide, you've already used this exact pattern in practice; the object-oriented Proxy and the infrastructure "reverse proxy" share far more than a name.

Proxy vs. Decorator vs. Adapter: Same Shape, Different Intent

These three patterns are a classic source of confusion because, structurally, a Proxy, a Decorator, and an Adapter all look nearly identical — a class implementing an interface, wrapping another object that implements the same or a related interface. The difference is entirely about intent, not code shape.

  • A Proxy exposes the exact same interface as the thing it wraps, and its job is to control access — deciding whether and how a call reaches the real object.
  • A Decorator also exposes the same interface, but its job is to add behavior around every call unconditionally — logging, compression, extra formatting — with no notion of "refusing" a call.
  • An Adapter exposes a different interface from the thing it wraps, translating calls from the shape a caller expects into the shape some existing object actually provides.

A caching layer in front of a service is arguably both a Proxy (controlling whether the real call happens) and a Decorator (adding caching behavior) at once — the pattern names describe intent and are meant to help you talk about a design, not to be mutually exclusive labels you have to pick exactly one of for a given class.

Proxies You're Probably Already Using

JavaScript has the pattern built directly into the language as Proxy, a global object that lets you intercept property access, assignment, and function calls on any object with get/set/apply traps — Vue 3's reactivity system is built on exactly this, wrapping your data objects in a Proxy so that reading a property can track it as a dependency and writing one can trigger a re-render, all without your component code doing anything different from plain object access.

Python's unittest.mock.Mock is a protection/recording proxy in disguise — it exposes whatever interface you ask of it, records every call made to it, and can be configured to raise or return specific values instead of doing real work, which is precisely "same interface, intercept and decide" applied to testing. API gateways in a microservices architecture are a remote proxy applied to an entire service boundary: client code calls one URL, and the gateway decides which internal service actually handles it, adding auth, rate limiting, and logging along the way without the caller needing to know any of that routing exists.

Frequently Asked Questions

Is a reverse proxy the same thing as the Proxy design pattern? They share the identical structural idea — one thing standing in for another behind the same interface — but reverse proxy specifically refers to network infrastructure like Nginx sitting in front of backend servers, while the Proxy pattern is a general object-oriented design concept that can be applied inside a single process with no network involved at all.
What's the difference between a Proxy and a Facade? A Facade simplifies a complicated set of interfaces into one easier interface, usually combining several subsystems. A Proxy keeps the exact same interface as the single object it wraps and never changes what the caller sees, only whether or how the call is fulfilled.
Does a caching proxy always need to know how to invalidate its cache? Yes, in production — a cache with no expiration or invalidation strategy will eventually serve stale data once the underlying value changes. The demo in this article deliberately skips invalidation to isolate and measure the caching effect itself.
Can one object be more than one type of proxy at once? Yes — a single wrapper can check permissions (protection), only construct the real object on first use (virtual), and cache results (caching) all in the same class. The GoF categories describe purposes a proxy can serve, not mutually exclusive implementations.
Is JavaScript's built-in Proxy object the same as the design pattern? Yes, directly — JavaScript's Proxy() is a language-level implementation of exactly this pattern, letting you intercept operations like property get/set on any object via traps, which is how libraries like Vue 3 implement reactivity.