Design PatternsIntermediate 12 min Lesson 8 of 13

Proxy — A Stand-in That Controls Access

Put a stand-in in front of an object to control access, delay expensive work, or enforce permissions.

Design Patterns · Lesson 8 of 13
0/13 done(0%)

What is it? #

A Proxy stands in for another object, implementing the same interface and controlling access to it.

It looks very like a Decorator, and the code often is similar. The difference is intent. A decorator adds behaviour to something you already have; a proxy controls whether, when or how the real object is used at all.

Three common kinds. A virtual proxy delays creating an expensive object until it is actually needed. A protection proxy checks permissions before allowing a call. A remote proxy makes a call to another machine look like a local one.

The caller sees the same interface throughout and never learns which it is holding.

Think of it like this #

A personal assistant. Calls to the executive go through them. Some are handled without ever disturbing the executive, some are refused, and some are put through.

The caller dials the same number either way. What changes is what happens behind it.

Simple example #

A report object is expensive to build — it queries several tables. Many pages display only the report title, so building the full report every time is wasteful. Separately, some reports may only be opened by managers.

Code #

PYTHON
import time
from abc import ABC, abstractmethod


class Report(ABC):
    @abstractmethod
    def title(self) -> str: ...
    @abstractmethod
    def rows(self) -> list[dict]: ...


class RealReport(Report):
    def __init__(self, report_id: str):
        self.report_id = report_id
        print(f"building report {report_id} (expensive)")
        time.sleep(0.2)                                   # heavy queries
        self._rows = [{"month": "Sep", "total": 120000}]

    def title(self): return f"Sales report {self.report_id}"
    def rows(self): return self._rows


# --- Virtual proxy: build the real thing only when rows are needed ---
class LazyReport(Report):
    def __init__(self, report_id: str):
        self.report_id = report_id
        self._real: RealReport | None = None

    def _load(self) -> RealReport:
        if self._real is None:
            self._real = RealReport(self.report_id)
        return self._real

    def title(self):
        return f"Sales report {self.report_id}"           # no loading needed

    def rows(self):
        return self._load().rows()                        # loads on first use


# --- Protection proxy: check permission before delegating ---
class RestrictedReport(Report):
    def __init__(self, inner: Report, user_role: str):
        self._inner, self._role = inner, user_role

    def title(self):
        return self._inner.title()

    def rows(self):
        if self._role not in {"manager", "admin"}:
            raise PermissionError("you are not allowed to view report data")
        return self._inner.rows()


listing = LazyReport("R-2026-09")
print(listing.title())            # no expensive build happens

secured = RestrictedReport(listing, user_role="analyst")
print(secured.title())            # fine
try:
    secured.rows()                # PermissionError, and still no build
except PermissionError as exc:
    print(exc)

manager_view = RestrictedReport(listing, user_role="manager")
print(manager_view.rows())        # now the real report is built, once
TEXT
Proxy vs Decorator

decorator  adds behaviour around an object you already created
proxy      controls access to, creation of, or location of the real object

Both implement the same interface as what they wrap.

How it works #

LazyReport can answer title() from the ID alone, so listing twenty reports costs nothing. RealReport is constructed only when rows() is first called, and self._real caches it so it is built once.

That is the virtual proxy: the expensive work is deferred until it is genuinely needed, and skipped entirely if it never is.

RestrictedReport wraps any Report and checks the role before exposing data. Titles stay visible; data does not. Because the check sits in the proxy, no caller can forget it.

Stacking the two gives a useful property: an unauthorised request fails before the expensive report is ever built. The order of wrapping produced that behaviour.

Both classes implement Report, so anything expecting a report works with either. The caller cannot tell it is holding a proxy.

The proxy-versus-decorator distinction is about intent rather than mechanics, and in real code the line is sometimes blurry. What matters is that the reader can tell which question the class answers: "what extra happens?" or "should this happen at all?"

Real-world use #

ORMs use virtual proxies extensively. Accessing order.customer may trigger a query the first time — lazy loading — which is convenient and also the cause of the N+1 query problem when it happens inside a loop.

Protection proxies show up as permission wrappers around resources, and API gateways are protection proxies at network scale: authentication, rate limiting and routing in front of your services.

Remote proxies make a network service look like a local object. gRPC and similar frameworks generate exactly this from a service definition.

Caching proxies sit in front of expensive services, and reverse proxies such as Nginx are the same idea at the infrastructure level — covered in the System Design and VPS tracks.

Common mistakes #

  • Hiding expensive work behind an innocent-looking property access, causing N+1 queries.
  • Forgetting to cache the loaded object, so it is rebuilt on every call.
  • Letting the proxy diverge from the interface it is standing in for.
  • Using a proxy for access control while leaving a direct path to the real object.
  • Confusing readers by combining unrelated concerns in one proxy.

Practice #

Write a CachedApiClient proxy around a client with fetch(path), caching responses for sixty seconds. Then add a RateLimitedClient proxy allowing at most five calls per minute. Stack them and describe what each ordering does differently.

Quick quiz

  1. 1. How does a Proxy differ from a Decorator?

  2. 2. What is a virtual proxy for?

  3. 3. Why cache the loaded object inside a lazy proxy?

  4. 4. What real problem does ORM lazy loading commonly cause?

  5. 5. Which infrastructure component is a proxy?

Summary

  • A proxy implements the same interface and controls access to the real object.
  • Virtual proxies defer expensive creation; protection proxies enforce permissions.
  • Decorator adds behaviour, proxy controls access — same shape, different intent.
  • Cache the loaded object or laziness becomes repeated work.
  • API gateways and ORM lazy loading are proxies you already use.