Design Patterns, Explained Simply · Structural Patterns: Fitting Objects Together

Proxy

Put a stand-in in front of an object to control access to it: load it lazily, check permissions, cache results, or call it over the network, without the client noticing.

The problem

A dashboard can show a SalesReport that takes several seconds to build from the data warehouse. The page creates the report object in its constructor, even when the user never opens the report tab, so every page load is slow.

The report also contains revenue numbers that only managers may see, and permission checks are scattered across every page that uses it. One page forgot the check.

Real-world analogy

A credit card is a proxy for your bank account. A shop treats it like money ("pay 20 dollars"), but behind it the card checks your PIN (access control), contacts the bank only when you actually pay (lazy), and can be blocked without closing the account.

Without the pattern

class Dashboard:
    def __init__(self, user):
        self.report = SalesReport()          # slow, even if never viewed
    def report_tab(self, user):
        if user.role != "manager":           # every page must remember this
            return "access denied"
        return self.report.summary()

The pattern

  • Subject interface: what clients use (summary()).
  • Real subject: the actual object (SalesReport).
  • Proxy: implements the same interface, holds (or creates) the real subject, and controls access before delegating.

Common kinds of proxy:

Kind What it controls
Virtual proxy Creates the expensive object lazily, on first use
Protection proxy Checks permissions before each call
Caching proxy Returns cached results for repeated requests
Remote proxy Hides that the object lives in another process (RPC stubs, SDK clients)
Logging/metrics proxy Records calls

Proxy and Decorator look alike in code. The difference is intent: a decorator adds features and is stacked by the client; a proxy controls access to its subject and usually manages its lifecycle.

The code

A proxy that combines lazy loading with a permission check:

A lazy, permission-checking proxy for an expensive reportTry it yourself
class SalesReport:  # real subject: expensive to create
    def __init__(self):
        print("  (building report from the warehouse...)")
        self.total = 182_500

    def summary(self):
        return f"Q3 revenue: ${self.total:,}"


class SalesReportProxy:  # same interface, controls access
    def __init__(self, user_role):
        self.user_role = user_role
        self._report = None  # not built yet

    def summary(self):
        if self.user_role != "manager":  # protection proxy
            return "access denied"
        if self._report is None:  # virtual proxy: build on first use
            self._report = SalesReport()
        return self._report.summary()


print("page loads for an analyst and a manager (no report built yet)")
analyst, manager = SalesReportProxy("analyst"), SalesReportProxy("manager")
print("analyst:", analyst.summary())
print("manager:", manager.summary())
print("manager again:", manager.summary())

Output

page loads for an analyst and a manager (no report built yet)
analyst: access denied
  (building report from the warehouse...)
manager: Q3 revenue: $182,500
manager again: Q3 revenue: $182,500

Page loads are instant, the report is built once and only for the manager, and the permission rule lives in one class.

When to use it

  • Lazy initialisation of expensive objects (large files, reports, ORM relations).
  • Access control in one place instead of every caller.
  • Caching or rate limiting results of an expensive or remote service.
  • Remote objects: hiding network calls behind a local-looking interface.

When not to use it

  • The object is cheap and has no access rules. A proxy only adds a layer.
  • Remote proxies can hide latency and failure: code that looks like a local method call may take 200 ms or fail with a timeout. Make remote calls obvious when that matters.
  • Lazy loading can move slowness to a surprising place (the classic N+1 query problem from lazily loaded ORM relations).

You've already used it

  • ORMs: Hibernate and Doctrine return lazy-loading proxies for related entities; Django and Eloquent relations load on first access.
  • JavaScript: the built-in Proxy object intercepts property access; Vue 3's reactivity system is built on it.
  • Spring: @Transactional and @Cacheable work by wrapping your beans in generated proxies.
  • Remote clients: gRPC stubs and cloud SDK clients look like local objects but call over the network.

Key points

  • A proxy has the same interface as its subject and controls access to it.
  • Kinds: virtual (lazy), protection (permissions), caching, remote, logging.
  • Proxy controls access; Decorator adds behaviour. The code looks similar, but the intent differs.
  • Watch for hidden latency (remote) and N+1 queries (lazy loading).
  • Further reading: Refactoring Guru: Proxy and MDN on the JavaScript Proxy object.

Practice questions

Name four kinds of proxy and what each controls.

Answer

Virtual proxy (creates an expensive object lazily), protection proxy (checks permissions), caching proxy (returns stored results), remote proxy (hides that the object is in another process, like an RPC stub). Logging/metrics proxies are also common.

What is the difference between Proxy and Decorator?

Answer

Both wrap an object with the same interface. A decorator adds behaviour and is usually stacked by the client in any combination. A proxy controls access to its subject (lazy creation, permissions, remote calls) and often manages the subject's lifecycle itself.

How can lazy-loading proxies in an ORM cause performance problems?

Answer

Looping over 100 orders and touching order.customer triggers one query per order, the N+1 query problem, because each related object is a lazy proxy loaded on first access. Use eager loading (select_related, with(), JOIN FETCH) when you know you'll need the relation.

How does Spring use the Proxy pattern?

Answer

Annotations like @Transactional, @Cacheable and @Async work because Spring wraps beans in generated proxies (JDK dynamic proxies or CGLIB subclasses) that run extra logic around each call. Calling another annotated method on this from inside the bean bypasses the proxy, a common gotcha.

Open in the app to mark lessons complete, take notes and track your progress.