The proxy pattern is a structural design pattern that provides a surrogate or placeholder for another object. The proxy implements the same interface as the real subject and controls access to it by deferring creation, enforcing permissions, caching results, or forwarding calls across a network. The client treats the proxy like the real object and does not need to know when work is delayed, guarded, or routed elsewhere.
The problem it solves
Some objects are expensive to create, slow to reach over the network, or should not be accessed without checks. Creating them eagerly at startup wastes memory and time. Exposing them directly lets callers bypass authorization or repeat costly operations on every call.
The proxy pattern inserts a stand-in with the same interface. The client keeps calling the familiar methods; the proxy decides when to create the real object, whether the caller is allowed, or whether a cached answer is good enough. The real subject stays unchanged; access policy and performance concerns live in the proxy.
Structure
- Subject, the interface shared by the real object and the proxy.
- Real subject, the object that does the actual work, often expensive or remote.
- Proxy, implements the subject interface, holds a reference to the real subject (or creates it on demand), and controls how and when calls reach it.
- Client, depends on the subject interface, unaware of whether it holds a proxy or the real object.
flowchart LR Client["Client"] -->|"depends on"| Subject["Subject interface"] Proxy["Proxy"] -.->|"implements"| Subject Proxy -->|"controls access to"| Real["RealSubject"]
In code
A dashboard displays reports stored on disk. Loading every report at startup is slow. A virtual proxy implements the same Report interface but creates the heavy RealReport only when render is first called.
// Subject: shared interface
class RealReport {
constructor(filename) {
this.filename = filename;
this.content = this.loadFromDisk(filename);
}
loadFromDisk(filename) {
console.log(`[disk] loading ${filename}`);
return `Contents of ${filename}`;
}
render() {
console.log(this.content);
}
}
// Proxy: defers creation until the report is actually needed
class ReportProxy {
constructor(filename) {
this.filename = filename;
this.realReport = null;
}
render() {
if (!this.realReport) {
this.realReport = new RealReport(this.filename);
}
this.realReport.render();
}
}
// Client depends only on the subject interface
function showReport(report) {
report.render();
}
const report = new ReportProxy("annual-summary.pdf");
// No disk read yet
showReport(report);
// [disk] loading annual-summary.pdf
// Contents of annual-summary.pdf// Subject: shared interface
interface Report {
render(): void;
}
class RealReport implements Report {
private readonly content: string;
constructor(private readonly filename: string) {
this.content = this.loadFromDisk(filename);
}
private loadFromDisk(filename: string): string {
console.log(`[disk] loading ${filename}`);
return `Contents of ${filename}`;
}
render(): void {
console.log(this.content);
}
}
// Proxy: defers creation until the report is actually needed
class ReportProxy implements Report {
private realReport?: RealReport;
constructor(private readonly filename: string) {}
render(): void {
if (!this.realReport) {
this.realReport = new RealReport(this.filename);
}
this.realReport.render();
}
}
// Client depends only on the subject interface
function showReport(report: Report): void {
report.render();
}
const report = new ReportProxy("annual-summary.pdf");
// No disk read yet
showReport(report);
// [disk] loading annual-summary.pdf
// Contents of annual-summary.pdf// Subject: shared interface
public interface IReport
{
void Render();
}
public class RealReport : IReport
{
private readonly string _content;
public RealReport(string filename)
{
_content = LoadFromDisk(filename);
}
private static string LoadFromDisk(string filename)
{
Console.WriteLine($"[disk] loading {filename}");
return $"Contents of {filename}";
}
public void Render() => Console.WriteLine(_content);
}
// Proxy: defers creation until the report is actually needed
public class ReportProxy : IReport
{
private readonly string _filename;
private RealReport? _realReport;
public ReportProxy(string filename) => _filename = filename;
public void Render()
{
_realReport ??= new RealReport(_filename);
_realReport.Render();
}
}
// Client depends only on the subject interface
void ShowReport(IReport report) => report.Render();
var report = new ReportProxy("annual-summary.pdf");
// No disk read yet
ShowReport(report);
// [disk] loading annual-summary.pdf
// Contents of annual-summary.pdffrom typing import Protocol, Optional
# Subject: shared interface
class Report(Protocol):
def render(self) -> None: ...
class RealReport:
def __init__(self, filename: str) -> None:
self._content = self._load_from_disk(filename)
def _load_from_disk(self, filename: str) -> str:
print(f"[disk] loading {filename}")
return f"Contents of {filename}"
def render(self) -> None:
print(self._content)
# Proxy: defers creation until the report is actually needed
class ReportProxy:
def __init__(self, filename: str) -> None:
self._filename = filename
self._real_report: Optional[RealReport] = None
def render(self) -> None:
if self._real_report is None:
self._real_report = RealReport(self._filename)
self._real_report.render()
# Client depends only on the subject interface
def show_report(report: Report) -> None:
report.render()
report = ReportProxy("annual-summary.pdf")
# No disk read yet
show_report(report)
# [disk] loading annual-summary.pdf
# Contents of annual-summary.pdfCommon proxy types
The same structural idea appears in several flavors, distinguished by what the proxy controls:
- Virtual proxy, defers creation or loading until the client actually needs the object (lazy initialization, as in the example above).
- Protection proxy, checks permissions or preconditions before forwarding a call to the real subject.
- Remote proxy, stands in for an object in another process or on another machine, hiding serialization and network details.
- Caching proxy, stores results of expensive calls and returns cached answers when inputs repeat.
These are intent labels, not separate patterns. One proxy class can combine virtual loading with caching or access checks when the use case calls for it.
Relationship to SOLID
Proxy supports the SOLID principles. It applies single responsibility by isolating access control, lazy loading, or remote communication from the real subject’s core logic. It supports open/closed because new proxy types can be added without changing the real subject or the client. It also follows dependency inversion when the client depends on the subject abstraction and receives either the real object or a proxy through that interface.
Proxy vs alternatives
- Decorator pattern, both wrap an object and share its interface, but a decorator adds behavior around every call; a proxy controls access to the subject (when it exists, who may use it, or where it lives). Decorators stack responsibilities; proxies usually stand alone as a gatekeeper.
- Adapter pattern, an adapter changes the interface so incompatible types can collaborate; a proxy keeps the same interface and manages how the real subject is reached.
- Facade pattern, a facade defines a simpler API over many subsystem classes; a proxy represents one real subject and preserves its interface.
- Flyweight, flyweight shares intrinsic state across many fine-grained objects to save memory; a proxy controls access to a single subject, often deferring its creation entirely.
Trade-offs
- Extra indirection and another class to maintain; when the real object is always needed immediately and access is unrestricted, a proxy adds complexity without benefit.
- Proxies that cache or lazy-load must handle staleness, invalidation, and thread safety explicitly. The pattern does not solve those concerns by itself.
- Remote proxies hide network failures and latency only up to a point; callers may still need timeouts, retries, and circuit breaking at the boundary.
- Best when creation or access is costly, when permissions should be enforced at a single choke point, or when the client should stay unaware of whether the subject is local, remote, or not yet loaded.
