Proxy Pattern
Provide a surrogate or placeholder for another object to control access to it. A structural design pattern for access control, lazy loading, and logging.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Proxy Pattern
Overview
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object. The proxy controls access to the real subject, adding a layer of indirection that can be used for lazy loading, access control, caching, logging, or monitoring — without changing the subject’s code.
When to Use
Use the Proxy Pattern when:
- You need lazy initialization for expensive objects (create on first use)
- You want to control access rights to an object (authentication, authorization)
- You need to cache results of expensive operations
- You want to log or monitor calls to an object transparently
- The real object is remote and you need a local representative
Solution
Python
from abc import ABC, abstractmethod
class Image(ABC):
@abstractmethod
def display(self):
pass
class RealImage(Image):
def __init__(self, filename: str):
self.filename = filename
self._load_from_disk()
def _load_from_disk(self):
print(f"Loading image: {self.filename}")
def display(self):
print(f"Displaying image: {self.filename}")
class ImageProxy(Image):
def __init__(self, filename: str):
self.filename = filename
self._real_image = None
def display(self):
if self._real_image is None:
self._real_image = RealImage(self.filename)
self._real_image.display()
# Usage: expensive object is not loaded until needed
proxy = ImageProxy("photo.jpg") # No loading yet
proxy.display() # Loads and displays
proxy.display() # Uses cached RealImage
JavaScript
class RealImage {
constructor(filename) {
this.filename = filename;
this.loadFromDisk();
}
loadFromDisk() {
console.log(`Loading image: ${this.filename}`);
}
display() {
console.log(`Displaying image: ${this.filename}`);
}
}
class ImageProxy {
constructor(filename) {
this.filename = filename;
this.realImage = null;
}
display() {
if (!this.realImage) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
}
// Usage
const proxy = new ImageProxy("photo.jpg");
proxy.display(); // Lazy loads
proxy.display(); // Uses cached instance
Java
public interface Image {
void display();
}
public class RealImage implements Image {
private final String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading image: " + filename);
}
@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}
public class ImageProxy implements Image {
private final String filename;
private RealImage realImage;
public ImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
// Usage
Image proxy = new ImageProxy("photo.jpg");
proxy.display(); // Lazy loads
proxy.display(); // Reuses cached RealImage
Explanation
The Proxy Pattern involves three roles:
- Subject Interface (
Image): The common interface shared by both the real object and the proxy - Real Subject (
RealImage): The actual object that performs the real work - Proxy (
ImageProxy): Controls access to the real subject, adding behavior before or after forwarding requests
The proxy can intercept operations to add caching, logging, access control, or lazy initialization transparently.
Variants
| Variant | Purpose | Example |
|---|---|---|
| Virtual Proxy | Lazy initialization | Loading large images on demand |
| Protection Proxy | Access control | Checking permissions before method execution |
| Caching Proxy | Memoization | Caching API responses or computed results |
| Remote Proxy | Network transparency | Local stub for a remote service |
| Smart Reference | Reference counting | Tracking object usage for cleanup |
What Works
- Keep the proxy interface identical to the real subject — clients should not know they are using a proxy
- Use lazy initialization only when the real object is expensive — otherwise, the proxy adds unnecessary complexity
- Handle thread safety in caching proxies when multiple clients may access shared cached data
- Implement reference counting in smart proxies to manage lifecycle of expensive resources
- Document proxy behavior (e.g., “this proxy caches results for 5 minutes”) so callers understand performance characteristics
Common Mistakes
- Exposing the proxy’s internal state or letting clients bypass it to reach the real subject directly
- Using a proxy when a simple decorator or direct reference would suffice, adding unnecessary indirection
- Forgetting to handle exceptions in the proxy, letting failures silently bypass logging or cleanup logic
- Implementing caching proxies without cache invalidation, leading to stale data
- Not synchronizing access in multi-threaded environments, causing race conditions in lazy initialization
Frequently Asked Questions
Q: What is the difference between Proxy and Decorator? A: Both wrap objects and add behavior. Proxy controls access to the wrapped object (often for structural reasons like lazy loading or remote access). See Caching Proxy for a concrete example. Decorator adds responsibilities dynamically, usually for functional enhancement. The intent differs even if the structure looks similar.
Q: Can a proxy wrap another proxy? A: Yes. You can stack proxies — for example, a caching proxy wrapping a remote proxy. Each layer adds its own behavior. Keep the stack shallow to avoid confusing stack traces and performance overhead.
Q: When should I use a Proxy instead of a Factory? A: Use a Factory when you want to control which class is instantiated. Use a Proxy when you want to control access to an already-instantiated object or delay its creation.
Is this pattern suitable for small projects?
For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.
How does this pattern compare to alternatives?
Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.
Can I partially apply this pattern?
Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.
Advanced Topics
Scenario: Proxy for Cache and Access Control
// Proxy pattern: control access to an object
interface DataService {
getData(key: string): Promise<unknown>;
setData(key: string, value: unknown): Promise<void>;
}
// Real subject: service that reads from DB
class DatabaseService implements DataService {
constructor(private db: Pool) {}
async getData(key: string): Promise<unknown> {
const res = await this.db.query("SELECT value FROM cache WHERE key = $1", [key]);
return res.rows[0]?.value || null;
}
async setData(key: string, value: unknown): Promise<void> {
await this.db.query("INSERT INTO cache (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", [key, JSON.stringify(value)]);
}
}
// Proxy 1: Cache
class CacheProxy implements DataService {
private cache = new Map<string, { value: unknown; expiry: number }>();
constructor(private real: DataService, private ttlMs: number = 60000) {}
async getData(key: string): Promise<unknown> {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) return cached.value;
const value = await this.real.getData(key);
this.cache.set(key, { value, expiry: Date.now() + this.ttlMs });
return value;
}
async setData(key: string, value: unknown): Promise<void> {
await this.real.setData(key, value);
this.cache.delete(key); // invalidate cache on write
}
}
// Proxy 2: Access control
class AccessProxy implements DataService {
constructor(private real: DataService, private userRole: string) {}
async getData(key: string): Promise<unknown> {
if (this.userRole !== "admin" && this.userRole !== "reader") {
throw new Error("Access denied: insufficient permissions");
}
return this.real.getData(key);
}
async setData(key: string, value: unknown): Promise<void> {
if (this.userRole !== "admin") {
throw new Error("Access denied: write requires admin");
}
await this.real.setData(key, value);
}
}
// Proxy 3: Logging
class LoggingProxy implements DataService {
constructor(private real: DataService) {}
async getData(key: string): Promise<unknown> {
const start = Date.now();
const result = await this.real.getData(key);
console.log(`[PROXY] getData(${key}) ${Date.now() - start}ms`);
return result;
}
async setData(key: string, value: unknown): Promise<void> {
await this.real.setData(key, value);
console.log(`[PROXY] setData(${key})`);
}
}
// Composition: Logging -> Access -> Cache -> DB
const service = new LoggingProxy(
new AccessProxy(
new CacheProxy(
new DatabaseService(dbPool)
),
currentUser.role
)
);
Lessons:
- Proxy controls access to the real object without changing its interface
- Types: virtual (lazy load), protection (access control), remote (RPC), smart (cache)
- Proxies compose: logging + access + cache + DB
- The client does not know it is talking to a proxy
- Proxy vs Decorator: Proxy controls access; Decorator adds behavior
### Proxy vs Decorator: which do I use?
Proxy controls access to the object: decides if, when, and how to access. Decorator adds behavior: always delegates to the real object. Proxy decides (access, cache, lazy); Decorator enriches (logging, metrics, retry). Proxy does not add new functionality: it only controls. Decorator adds. Use Proxy for access control, cache, lazy loading. Use Decorator for logging, metrics, additional validation. Related Resources
Decorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
PatternAdapter Pattern
Convert the interface of a class into another interface clients expect. A structural design pattern for interface compatibility.
PatternFactory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternBusiness Delegate Pattern
Reduce coupling between presentation and business tiers by introducing an intermediary that handles lookup, creation, and invocation of business services.
PatternCircuit Breaker Pattern
Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.
PatternFacade Pattern
Provide a simplified interface to a complex subsystem. A structural pattern that hides implementation details behind a clean API.
PatternFlyweight Pattern
Share objects to support large numbers of fine-grained objects efficiently. A structural design pattern for memory optimization.