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:
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
interface Report {
summary(): string;
}
class SalesReport implements Report { // real subject: expensive to create
total = 182_500;
constructor() { console.log(" (building report from the warehouse...)"); }
summary() { return `Q3 revenue: $${this.total.toLocaleString("en-US")}`; }
}
class SalesReportProxy implements Report { // same interface, controls access
private report: SalesReport | null = null; // not built yet
constructor(private userRole: string) {}
summary(): string {
if (this.userRole !== "manager") return "access denied"; // protection proxy
this.report ??= new SalesReport(); // virtual proxy: build on first use
return this.report.summary();
}
}
console.log("page loads for an analyst and a manager (no report built yet)");
const analyst = new SalesReportProxy("analyst");
const manager = new SalesReportProxy("manager");
console.log("analyst:", analyst.summary());
console.log("manager:", manager.summary());
console.log("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
public class Main {
public static void main(String[] args) {
System.out.println("page loads for an analyst and a manager (no report built yet)");
Report analyst = new SalesReportProxy("analyst");
Report manager = new SalesReportProxy("manager");
System.out.println("analyst: " + analyst.summary());
System.out.println("manager: " + manager.summary());
System.out.println("manager again: " + manager.summary());
}
}
interface Report {
String summary();
}
class SalesReport implements Report { // real subject: expensive to create
private final int total;
SalesReport() {
System.out.println(" (building report from the warehouse...)");
total = 182_500;
}
public String summary() { return String.format("Q3 revenue: $%,d", total); }
}
class SalesReportProxy implements Report { // same interface, controls access
private final String userRole;
private SalesReport report; // not built yet
SalesReportProxy(String userRole) { this.userRole = userRole; }
public String summary() {
if (!userRole.equals("manager")) return "access denied"; // protection proxy
if (report == null) report = new SalesReport(); // virtual proxy: build on first use
return report.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
Run this on your computer — see “Get Started” for setup.
<?php
interface Report {
public function summary(): string;
}
class SalesReport implements Report { // real subject: expensive to create
private int $total;
public function __construct() {
echo " (building report from the warehouse...)\n";
$this->total = 182_500;
}
public function summary(): string { return "Q3 revenue: $" . number_format($this->total); }
}
class SalesReportProxy implements Report { // same interface, controls access
private ?SalesReport $report = null; // not built yet
public function __construct(private string $userRole) {}
public function summary(): string {
if ($this->userRole !== "manager") return "access denied"; // protection proxy
$this->report ??= new SalesReport(); // virtual proxy: build on first use
return $this->report->summary();
}
}
echo "page loads for an analyst and a manager (no report built yet)\n";
$analyst = new SalesReportProxy("analyst");
$manager = new SalesReportProxy("manager");
foreach (["analyst" => $analyst, "manager" => $manager, "manager again" => $manager] as $label => $proxy) {
$text = $proxy->summary(); // evaluate first: echo prints its arguments one by one
echo "$label: $text\n";
}
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
Proxyobject intercepts property access; Vue 3's reactivity system is built on it. - Spring:
@Transactionaland@Cacheablework 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.