The Proxy pattern puts a surrogate object in front of the real object to control access to it. The proxy has the same interface as the real object, so clients don’t even know they’re talking to a proxy.
Think of it like a security guard at a building entrance. We don’t interact with the building directly. The guard checks our ID, decides if we can enter, maybe logs our visit — all before letting us through to the actual building.
The Problem It Solves
Sometimes we can’t or don’t want to give clients direct access to an object:
- The object is expensive to create and we want to delay it until it’s actually needed (lazy loading)
- We need to check permissions before allowing access
- We want to cache results to avoid repeated expensive operations
- We need to log every access to the object
A proxy handles all of this without the client knowing anything changed.
Types of Proxy
Virtual Proxy — Lazy Image Loading
The image isn’t loaded until we actually call display(). This saves memory and startup time.
from abc import ABC, abstractmethod
# Common interface
class Image(ABC):
@abstractmethod
def display(self) -> None:
pass
# Real image -- expensive to create (loads from disk)
class RealImage(Image):
def __init__(self, filename: str):
self.filename = filename
self._load_from_disk() # Slow! Happens at creation time
def _load_from_disk(self):
print(f"Loading {self.filename} from disk... (slow)")
def display(self):
print(f"Displaying {self.filename}")
# Proxy -- delays loading until display() is called
class ImageProxy(Image):
def __init__(self, filename: str):
self.filename = filename
self._real_image = None # not loaded yet!
def display(self):
if self._real_image is None:
self._real_image = RealImage(self.filename) # load on first use
self._real_image.display()
# Without proxy: image loads immediately (even if never displayed)
# With proxy: image loads ONLY when display() is called
gallery = [ImageProxy(f"photo_{i}.jpg") for i in range(100)]
# Nothing loaded yet! Memory is clean.
gallery[0].display() # NOW photo_0.jpg loads
gallery[0].display() # Already loaded, just displays
// Real image -- expensive to create
class RealImage {
constructor(filename) {
this.filename = filename;
this.#loadFromDisk();
}
#loadFromDisk() {
console.log(`Loading ${this.filename} from disk... (slow)`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
// Proxy -- delays loading
class ImageProxy {
#filename;
#realImage = null;
constructor(filename) {
this.#filename = filename;
}
display() {
if (!this.#realImage) {
this.#realImage = new RealImage(this.#filename);
}
this.#realImage.display();
}
}
// Create 100 proxies -- nothing loads
const gallery = Array.from({ length: 100 },
(_, i) => new ImageProxy(`photo_${i}.jpg`)
);
gallery[0].display(); // NOW it loads
gallery[0].display(); // Already loaded
interface Image {
void display();
}
class RealImage implements Image {
private String filename;
RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading " + filename + " from disk... (slow)");
}
public void display() {
System.out.println("Displaying " + filename);
}
}
class ImageProxy implements Image {
private String filename;
private RealImage realImage;
ImageProxy(String filename) {
this.filename = filename;
// realImage is null -- not loaded yet
}
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // load on first use
}
realImage.display();
}
}
Protection Proxy — Access Control
Only users with the right role can perform certain actions.
class Document:
def __init__(self, content: str):
self.content = content
def read(self) -> str:
return self.content
def write(self, content: str):
self.content = content
class DocumentProxy:
def __init__(self, document: Document, user_role: str):
self._document = document
self._user_role = user_role
def read(self) -> str:
# Everyone can read
return self._document.read()
def write(self, content: str):
# Only admins can write
if self._user_role != "admin":
raise PermissionError("Only admins can edit this document")
self._document.write(content)
doc = Document("Secret stuff")
viewer = DocumentProxy(doc, "viewer")
admin = DocumentProxy(doc, "admin")
print(viewer.read()) # "Secret stuff" -- works
admin.write("Updated") # works
# viewer.write("Hacked!") # raises PermissionError
class Document {
constructor(content) { this.content = content; }
read() { return this.content; }
write(content) { this.content = content; }
}
class DocumentProxy {
#document;
#userRole;
constructor(document, userRole) {
this.#document = document;
this.#userRole = userRole;
}
read() {
return this.#document.read();
}
write(content) {
if (this.#userRole !== "admin") {
throw new Error("Only admins can edit this document");
}
this.#document.write(content);
}
}
const doc = new Document("Secret stuff");
const viewer = new DocumentProxy(doc, "viewer");
const admin = new DocumentProxy(doc, "admin");
console.log(viewer.read()); // "Secret stuff"
admin.write("Updated"); // works
// viewer.write("Hacked!"); // throws Error
class Document {
private String content;
Document(String content) { this.content = content; }
String read() { return content; }
void write(String content) { this.content = content; }
}
class DocumentProxy {
private Document document;
private String userRole;
DocumentProxy(Document document, String userRole) {
this.document = document;
this.userRole = userRole;
}
String read() {
return document.read();
}
void write(String content) {
if (!"admin".equals(userRole)) {
throw new SecurityException("Only admins can edit");
}
document.write(content);
}
}
Proxy vs Decorator vs Adapter
These three look similar because they all wrap objects. Here’s how to tell them apart:
| Pattern | Purpose | Key Difference |
|---|---|---|
| Proxy | Control access to an object | Same interface, manages lifecycle/access |
| Decorator | Add new behavior to an object | Same interface, adds functionality |
| Adapter | Make incompatible interfaces work | Different interface, translates |
The easiest way to remember: Proxy controls, Decorator enhances, Adapter translates.
When to Use
- Lazy initialization — delay expensive object creation until needed
- Access control — check permissions before forwarding requests
- Caching — store expensive results and reuse them
- Logging/monitoring — track every access to an object
- Remote proxy — represent an object that lives on a different server (like RPC stubs)
When NOT to Use
- When there’s no reason to control access — adding a proxy “just in case” is overengineering
- When the overhead of an extra layer hurts performance more than it helps
- When the real object is cheap to create and has no access restrictions
In simple language, a Proxy is a bodyguard standing in front of the real object. It looks just like the real thing from the outside (same interface), but it decides when, how, and if we get access. Lazy loading, permission checks, caching — the proxy handles all of it, and the client never knows it’s not talking to the real thing.