Proxy
Provides a surrogate or placeholder for another object to control access to it.
Proxy Pattern: A Complete Guide with Examples in 11 Programming Languages
The Proxy Pattern is a structural design pattern that provides a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform operations before or after a request reaches the target — without the client knowing it is dealing with a proxy at all. Whether you need lazy initialization, access control, request logging, caching, remote object representation, or smart reference counting, the Proxy Pattern gives you a transparent layer of indirection that keeps your core object clean and your cross-cutting concerns organized.
In this comprehensive guide, we'll explore the Proxy Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is the Proxy Pattern?
- Why Use the Proxy Pattern?
- Proxy Pattern Comparison
- Proxy Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Proxy Pattern?
The Proxy Pattern places an intermediary object in front of a real object, sharing the same interface. Clients talk to the proxy thinking they're talking to the real object. The proxy decides whether and how to forward requests to the real object, and can add behavior before or after forwarding.
Think of it like a personal assistant. When people want to reach a busy executive, they go through the assistant first. The assistant filters calls, schedules meetings, and handles routine requests — sometimes without ever involving the executive. The assistant is the proxy.
Why Use the Proxy Pattern?
The Proxy Pattern offers several compelling benefits:
- Lazy Initialization (Virtual Proxy): Delay creating a heavy object until it is actually needed
- Access Control (Protection Proxy): Check permissions before forwarding a request
- Caching (Caching Proxy): Cache results of expensive operations and return cached values on repeat calls
- Logging and Auditing: Record what operations are performed and by whom
- Remote Access (Remote Proxy): Represent an object that lives in another process or machine
- Smart References: Perform additional actions when an object is accessed (e.g., reference counting, null checks)
- Rate Limiting: Throttle how frequently the real object can be called
Proxy Pattern Comparison
Let's compare Proxy with related structural patterns:
| Pattern | Purpose | Interface | Awareness | Use Case |
|---|---|---|---|---|
| Proxy | Control access to real object | Same as real object | Client unaware of proxy | Lazy init, auth, caching, logging |
| Decorator | Add behavior to object | Same as wrapped object | Client may know | Extending functionality dynamically |
| Facade | Simplify complex subsystem | New simplified interface | Client uses facade intentionally | Reducing complexity for clients |
| Adapter | Convert incompatible interface | Different from adaptee | Client uses adapter intentionally | Bridging incompatible APIs |
| Flyweight | Share state to save memory | Same as shared object | Client may know | Millions of fine-grained objects |
Key Distinctions:
- Proxy vs. Decorator: Both share the interface of the wrapped object, but Proxy controls access, while Decorator adds behavior. A proxy typically manages the lifecycle of its subject; a decorator does not.
- Proxy vs. Facade: Facade provides a simplified interface over many objects. Proxy provides the same interface over one object.
Proxy Pattern Explained
The Proxy Pattern typically involves:
Core Components:
- Subject Interface: Declares the common interface for both the real object and proxy. Clients program against this interface so they can work with either transparently.
- Real Subject: The actual object that does the real work. The proxy delegates to it.
- Proxy: Implements the subject interface and holds a reference to the real subject. Adds its logic before/after delegating.
- Client: Works with both the real subject and the proxy through the subject interface — ideally without knowing which one it has.
Common Proxy Types:
- Virtual Proxy: Defers creation of an expensive object until first use. The proxy stands in as a lightweight placeholder.
- Protection Proxy: Controls access based on permissions. Checks whether the caller has the right to perform the requested operation.
- Caching Proxy: Caches results of expensive calls and returns cached results on subsequent identical requests.
- Remote Proxy: Represents an object in a different address space (e.g., a gRPC/REST stub, RMI stub).
- Logging Proxy: Records calls to the real object for auditing, debugging, or analytics.
- Smart Reference Proxy: Performs cleanup when no references remain (reference counting), or pre-checks before forwarding (null safety, circuit breaking).
Class Diagram
Here's the UML class diagram showing the Proxy structure:
Beginner-Friendly Example
Let's start with a classic Virtual Proxy example: lazy-loading a large image. Loading a high-resolution image from disk is expensive. A ImageProxy stands in for the real image, loading it only when it actually needs to be displayed.
from abc import ABC, abstractmethod
import time
# Subject interface
class Image(ABC):
@abstractmethod
def display(self) -> None:
pass
@abstractmethod
def get_filename(self) -> str:
pass
# Real Subject — expensive to load
class HighResolutionImage(Image):
def __init__(self, filename: str) -> None:
self._filename = filename
self._load_from_disk()
def _load_from_disk(self) -> None:
print(f"[RealImage] Loading '{self._filename}' from disk... (expensive operation)")
time.sleep(0.1) # Simulate I/O
def display(self) -> None:
print(f"[RealImage] Displaying '{self._filename}'")
def get_filename(self) -> str:
return self._filename
# Virtual Proxy — defers loading until display() is called
class ImageProxy(Image):
def __init__(self, filename: str) -> None:
self._filename = filename
self._real_image: HighResolutionImage | None = None
def display(self) -> None:
if self._real_image is None:
print(f"[Proxy] First access — initializing real image...")
self._real_image = HighResolutionImage(self._filename)
self._real_image.display()
def get_filename(self) -> str:
return self._filename # Can serve this without loading the real image
# Client code
if __name__ == "__main__":
print("=== Creating image proxies (no loading yet) ===")
images = [
ImageProxy("photo_vacation.jpg"),
ImageProxy("photo_wedding.jpg"),
ImageProxy("photo_portrait.jpg"),
]
print("\n=== Accessing filenames (no loading) ===")
for img in images:
print(f" File: {img.get_filename()}")
print("\n=== Displaying first image (loads now) ===")
images[0].display()
print("\n=== Displaying first image again (already loaded) ===")
images[0].display()
print("\n=== Displaying second image (loads now) ===")
images[1].display()
Production-Ready Example
Now let's tackle a realistic scenario: a service layer with multiple proxy concerns — caching, access control, and logging — applied to a database-backed user repository. This demonstrates how proxies can be layered (chained) to compose cross-cutting behavior cleanly.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Dict, List
import time
import hashlib
@dataclass
class User:
id: int
name: str
email: str
role: str = "user"
@dataclass
class RequestContext:
user_id: int
role: str
request_id: str = field(default_factory=lambda: hashlib.md5(
str(time.time()).encode()).hexdigest()[:8])
# Subject Interface
class UserRepository(ABC):
@abstractmethod
def find_by_id(self, user_id: int) -> Optional[User]:
pass
@abstractmethod
def find_all(self) -> List[User]:
pass
@abstractmethod
def save(self, user: User) -> User:
pass
@abstractmethod
def delete(self, user_id: int) -> bool:
pass
# Real Subject — actual database operations
class DatabaseUserRepository(UserRepository):
def __init__(self):
# Simulated database
self._db: Dict[int, User] = {
1: User(1, "Alice", "alice@example.com", "admin"),
2: User(2, "Bob", "bob@example.com", "user"),
3: User(3, "Charlie", "charlie@example.com", "user"),
}
self._next_id = 4
def find_by_id(self, user_id: int) -> Optional[User]:
print(f" [DB] SELECT * FROM users WHERE id = {user_id}")
time.sleep(0.05) # Simulate DB latency
return self._db.get(user_id)
def find_all(self) -> List[User]:
print(" [DB] SELECT * FROM users")
time.sleep(0.1)
return list(self._db.values())
def save(self, user: User) -> User:
if user.id == 0:
user.id = self._next_id
self._next_id += 1
print(f" [DB] UPSERT user id={user.id}")
self._db[user.id] = user
return user
def delete(self, user_id: int) -> bool:
print(f" [DB] DELETE FROM users WHERE id = {user_id}")
return self._db.pop(user_id, None) is not None
# --- Proxy 1: Caching Proxy ---
class CachingUserRepository(UserRepository):
def __init__(self, real: UserRepository, ttl_seconds: float = 30.0):
self._real = real
self._ttl = ttl_seconds
self._cache: Dict[str, tuple] = {} # key -> (value, expiry)
def _get_cached(self, key: str):
entry = self._cache.get(key)
if entry and time.time() < entry[1]:
print(f" [Cache] HIT for key='{key}'")
return entry[0]
print(f" [Cache] MISS for key='{key}'")
return None
def _set_cached(self, key: str, value) -> None:
self._cache[key] = (value, time.time() + self._ttl)
def _invalidate(self, pattern: str) -> None:
keys = [k for k in self._cache if pattern in k]
for k in keys:
del self._cache[k]
if keys:
print(f" [Cache] Invalidated {len(keys)} entries matching '{pattern}'")
def find_by_id(self, user_id: int) -> Optional[User]:
key = f"user:{user_id}"
cached = self._get_cached(key)
if cached is not None:
return cached
result = self._real.find_by_id(user_id)
if result:
self._set_cached(key, result)
return result
def find_all(self) -> List[User]:
key = "users:all"
cached = self._get_cached(key)
if cached is not None:
return cached
result = self._real.find_all()
self._set_cached(key, result)
return result
def save(self, user: User) -> User:
result = self._real.save(user)
self._invalidate(f"user:{user.id}")
self._invalidate("users:all")
return result
def delete(self, user_id: int) -> bool:
result = self._real.delete(user_id)
self._invalidate(f"user:{user_id}")
self._invalidate("users:all")
return result
# --- Proxy 2: Protection Proxy ---
class ProtectedUserRepository(UserRepository):
def __init__(self, real: UserRepository, context: RequestContext):
self._real = real
self._ctx = context
def _check_role(self, required: str) -> None:
if self._ctx.role != required:
raise PermissionError(
f"Action requires role='{required}', "
f"but caller has role='{self._ctx.role}'"
)
def find_by_id(self, user_id: int) -> Optional[User]:
return self._real.find_by_id(user_id)
def find_all(self) -> List[User]:
return self._real.find_all()
def save(self, user: User) -> User:
self._check_role("admin")
return self._real.save(user)
def delete(self, user_id: int) -> bool:
self._check_role("admin")
return self._real.delete(user_id)
# --- Proxy 3: Logging Proxy ---
class LoggingUserRepository(UserRepository):
def __init__(self, real: UserRepository, context: RequestContext):
self._real = real
self._ctx = context
def _log(self, action: str, details: str = "") -> None:
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f" [Audit] {ts} | req={self._ctx.request_id} | "
f"caller={self._ctx.user_id} | action={action} {details}")
def find_by_id(self, user_id: int) -> Optional[User]:
self._log("find_by_id", f"target_id={user_id}")
result = self._real.find_by_id(user_id)
self._log("find_by_id", f"result={'found' if result else 'not_found'}")
return result
def find_all(self) -> List[User]:
self._log("find_all")
result = self._real.find_all()
self._log("find_all", f"count={len(result)}")
return result
def save(self, user: User) -> User:
self._log("save", f"user_id={user.id} name={user.name}")
result = self._real.save(user)
self._log("save", f"success id={result.id}")
return result
def delete(self, user_id: int) -> bool:
self._log("delete", f"target_id={user_id}")
result = self._real.delete(user_id)
self._log("delete", f"success={result}")
return result
# Factory: compose proxy chain
def create_user_repository(context: RequestContext) -> UserRepository:
"""
Builds the proxy chain:
Client → Logging → Protection → Caching → Database
"""
db = DatabaseUserRepository()
cached = CachingUserRepository(db, ttl_seconds=30.0)
guarded = ProtectedUserRepository(cached, context)
logged = LoggingUserRepository(guarded, context)
return logged
# Client code
if __name__ == "__main__":
# --- Admin context ---
admin_ctx = RequestContext(user_id=1, role="admin")
repo = create_user_repository(admin_ctx)
print("=== Admin: find user 2 (cache miss) ===")
user = repo.find_by_id(2)
print(f" Result: {user}")
print("\n=== Admin: find user 2 again (cache hit) ===")
user = repo.find_by_id(2)
print(f" Result: {user}")
print("\n=== Admin: save new user ===")
new_user = User(0, "Diana", "diana@example.com", "user")
saved = repo.save(new_user)
print(f" Saved: {saved}")
print("\n=== Admin: find all (cache miss after save) ===")
all_users = repo.find_all()
print(f" Count: {len(all_users)}")
# --- Regular user context ---
print("\n=== Regular user: find by id (allowed) ===")
user_ctx = RequestContext(user_id=2, role="user")
user_repo = create_user_repository(user_ctx)
result = user_repo.find_by_id(3)
print(f" Result: {result}")
print("\n=== Regular user: try to delete (blocked) ===")
try:
user_repo.delete(3)
except PermissionError as e:
print(f" Blocked: {e}")
Real-World Use Cases
1. ORM Lazy Loading
ORMs like SQLAlchemy (Python), Hibernate (Java), and Entity Framework (C#) use Virtual Proxies to represent related objects. A User.orders property doesn't load orders from the database until you actually iterate over them.
# SQLAlchemy uses lazy loading proxies transparently
user = session.query(User).get(1)
# orders are NOT loaded yet — user.orders is a proxy
for order in user.orders: # <-- DB query fires HERE, on first access
print(order.total)
2. Security / Access Control Layers
In enterprise applications, a Protection Proxy sits between the API controller and the service layer to enforce authorization rules without polluting business logic.
class SecurePaymentService:
def __init__(self, real: PaymentService, user: AuthUser):
self._real = real
self._user = user
def refund(self, transaction_id: str, amount: float) -> None:
if not self._user.has_permission("payments:refund"):
raise PermissionError("Insufficient permissions for refund")
if amount > 1000 and not self._user.has_permission("payments:large_refund"):
raise PermissionError("Large refund requires elevated permission")
self._real.refund(transaction_id, amount)
3. HTTP Client Caching and Retry
A Caching Proxy wraps an HTTP client, returning cached responses for identical GET requests and implementing retry logic for transient failures.
class ResilientHttpClient:
def __init__(self, real_client, cache_ttl=60, max_retries=3):
self._client = real_client
self._cache = {}
self._cache_ttl = cache_ttl
self._max_retries = max_retries
def get(self, url: str) -> Response:
if url in self._cache:
entry, expiry = self._cache[url]
if time.time() < expiry:
return entry # Cached response
for attempt in range(self._max_retries):
try:
response = self._client.get(url)
self._cache[url] = (response, time.time() + self._cache_ttl)
return response
except TransientError:
if attempt == self._max_retries - 1:
raise
time.sleep(2 ** attempt) # Exponential backoff
4. Remote Proxy (RPC / gRPC Stubs)
When you call a method on a gRPC stub, you are using a Remote Proxy. The stub looks like a local object but marshals arguments, sends them over the network, and unmarshals the response.
# gRPC-generated stub IS a remote proxy
stub = UserServiceStub(grpc.insecure_channel("api.example.com:50051"))
user = stub.GetUser(GetUserRequest(id=42)) # Looks local, runs remotely
5. JavaScript's Built-in Proxy Object
JavaScript has a native Proxy that implements the pattern at the language level, enabling dynamic interception of any property access, assignment, or method call.
const handler = {
get(target, prop) {
console.log(`[Proxy] Getting '${prop}'`);
return prop in target ? target[prop] : `Property '${prop}' not found`;
},
set(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
console.log(`[Proxy] Setting '${prop}' = ${value}`);
target[prop] = value;
return true;
}
};
const user = new Proxy({ name: "Alice", age: 30 }, handler);
console.log(user.name); // [Proxy] Getting 'name' → Alice
user.age = "thirty"; // Throws TypeError
6. Circuit Breaker
A Circuit Breaker is a Proxy that monitors the health of a downstream service. After too many consecutive failures it "opens" the circuit, failing fast without even attempting the real call — protecting the system from cascading failures.
class CircuitBreakerProxy:
def __init__(self, real_service, failure_threshold=5, recovery_timeout=30):
self._real = real_service
self._failures = 0
self._threshold = failure_threshold
self._opened_at = None
self._timeout = recovery_timeout
@property
def _is_open(self) -> bool:
if self._opened_at and time.time() - self._opened_at > self._timeout:
self._failures = 0 # Try half-open
self._opened_at = None
return False
return self._failures >= self._threshold
def call(self, *args, **kwargs):
if self._is_open:
raise ServiceUnavailableError("Circuit breaker is open")
try:
result = self._real.call(*args, **kwargs)
self._failures = 0
return result
except Exception as e:
self._failures += 1
if self._failures >= self._threshold:
self._opened_at = time.time()
raise
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Breaking the Interface — Proxy Does Not Fully Implement Subject
# BAD: Proxy only implements some methods — clients can't use it interchangeably
class ImageProxy:
def display(self): # Only this method — missing get_filename()
...
# If someone calls proxy.get_filename(), AttributeError!
# GOOD: Implement every method from the Subject interface
from abc import ABC, abstractmethod
class Image(ABC):
@abstractmethod
def display(self) -> None: ...
@abstractmethod
def get_filename(self) -> str: ...
class ImageProxy(Image): # Forces complete implementation
def display(self) -> None: ...
def get_filename(self) -> str: ...
❌ Mistake #2: Proxy Adds Business Logic It Shouldn't Own
# BAD: Caching proxy is also validating and transforming data — too many responsibilities
class CachingUserRepository:
def find_by_id(self, user_id: int):
if user_id <= 0: # Validation doesn't belong here
raise ValueError("...")
user = self._real.find_by_id(user_id)
user.name = user.name.title() # Transformation doesn't belong here either
return user
# GOOD: Caching proxy only caches; validation lives in domain/service layer
class CachingUserRepository:
def find_by_id(self, user_id: int):
cached = self._cache.get(user_id)
if cached:
return cached
result = self._real.find_by_id(user_id)
self._cache[user_id] = result
return result
❌ Mistake #3: Not Thread-Safe Lazy Initialization
# BAD: Two threads can both enter the if-block and create duplicate real objects
class ImageProxy:
def display(self):
if self._real_image is None:
self._real_image = HighResolutionImage(self._filename) # Race condition!
self._real_image.display()
# GOOD: Use threading.Lock for thread-safe lazy init
import threading
class ImageProxy:
def __init__(self, filename):
self._filename = filename
self._real_image = None
self._lock = threading.Lock()
def display(self):
if self._real_image is None:
with self._lock:
if self._real_image is None: # Double-check inside lock
self._real_image = HighResolutionImage(self._filename)
self._real_image.display()
Frequently Asked Questions
Q1: What is the difference between Proxy and Decorator?
Both patterns wrap an object and share its interface, which is why they're often confused. The key differences are intent and lifecycle:
Proxy controls access to the real subject and usually manages its lifecycle (creates it lazily, destroys it, owns it). The client typically doesn't know it's using a proxy.
Decorator adds behavior to an object. It doesn't control access — it enhances functionality. Decorators are often stacked and the client intentionally wraps objects with them.
A good rule of thumb: if you're asking "should I let this call through?" — that's a Proxy. If you're asking "what extra should I do alongside this call?" — that's a Decorator.
Q2: Can you stack multiple proxies together?
Yes — this is one of the most powerful aspects of the pattern. Because each proxy implements the same interface, proxies can be chained:
# Chain: Logging → Protection → Caching → Database
repo = LoggingProxy(
ProtectionProxy(
CachingProxy(DatabaseRepository(), ttl=30),
context=user_ctx
),
context=user_ctx
)
The order matters: in this chain, every call is logged first, then checked for permissions, then checked in cache, and finally reaches the database if needed.
Q3: Is Python's __getattr__ a form of Proxy?
Yes. Python allows you to intercept attribute access with __getattr__, __getattribute__, and __setattr__, which is effectively a dynamic proxy mechanism built into the language. The unittest.mock.Mock class is a great example — it proxies any attribute or method call dynamically.
class LoggingProxy:
def __init__(self, real):
self.__dict__['_real'] = real
def __getattr__(self, name):
attr = getattr(self._real, name)
if callable(attr):
def wrapper(*args, **kwargs):
print(f"[Log] Calling {name}")
return attr(*args, **kwargs)
return wrapper
return attr
Q4: What is a Remote Proxy?
A Remote Proxy represents an object that lives in a different process or machine. From the client's perspective, calling a method on it looks the same as calling a local object — the proxy handles all the networking, serialization, and deserialization transparently.
gRPC stubs, WCF proxies, and Java RMI stubs are all Remote Proxies. Modern service meshes (Envoy, Linkerd) act as Remote Proxies at the infrastructure level.
Q5: How does Spring AOP relate to the Proxy Pattern?
Spring AOP (Aspect-Oriented Programming) uses dynamic proxies (either JDK Proxy for interfaces or CGLIB for classes) to intercept method calls and apply cross-cutting concerns like @Transactional, @Cacheable, @Secured, and @Retryable. When you annotate a Spring bean with @Cacheable, Spring wraps it in a Caching Proxy automatically.
@Service
public class UserService {
@Cacheable("users") // Spring generates a CachingProxy transparently
public User findById(Long id) { return repo.findById(id); }
@Transactional // Spring generates a TransactionProxy transparently
public User save(User user) { return repo.save(user); }
}
Q6: When should I NOT use the Proxy Pattern?
Avoid it when:
- The indirection adds overhead that outweighs the benefit (e.g., wrapping a trivially cheap operation in a caching proxy).
- The subject interface is large — implementing every method in every proxy is tedious. Use delegation (
byin Kotlin,__getattr__in Python, dynamic proxies in Java) to reduce boilerplate. - You need to modify or extend behavior rather than control access — prefer Decorator.
- There is only one well-defined behavior to add and it won't change — inline the logic rather than creating a proxy class.
Q7: How do I test code that uses proxies?
Testing becomes easier with proxies, not harder. For unit tests, inject the real subject directly (bypass the proxy). For integration tests, use the full proxy chain. For testing the proxy itself, inject a mock real subject:
from unittest.mock import Mock
def test_caching_proxy_returns_cached_result():
mock_db = Mock(spec=UserRepository)
mock_db.find_by_id.return_value = User(1, "Alice", "alice@example.com")
proxy = CachingUserRepository(mock_db)
result1 = proxy.find_by_id(1)
result2 = proxy.find_by_id(1)
assert result1 == result2
assert mock_db.find_by_id.call_count == 1 # DB called only once
def test_protection_proxy_blocks_unauthorized():
mock_db = Mock(spec=UserRepository)
ctx = RequestContext(user_id=2, role="user")
proxy = ProtectedUserRepository(mock_db, ctx)
with pytest.raises(PermissionError):
proxy.delete(1)
mock_db.delete.assert_not_called() # Real delete never reached
Q8: What is a "Smart Reference" proxy?
A Smart Reference Proxy adds automatic behavior when the subject is accessed, beyond simple delegation. Examples include:
- Reference counting: Track how many clients hold a reference; clean up the subject when count reaches zero
- Null safety guard: Check if the real subject is still valid before delegating
- Copy-on-write: Delay copying a shared object until a client tries to modify it
- Weak reference tracking: Reload the subject if it has been garbage collected
Q9: Is lazy initialization always safe in the Proxy Pattern?
Not without care. In single-threaded code, the simple if null, create pattern is fine. In multi-threaded code, without synchronization, two threads can both observe null and both create the real subject — wasting resources or causing bugs. The standard solution is double-checked locking:
// Thread-safe lazy initialization in Java
public void display() {
if (realImage == null) {
synchronized (this) {
if (realImage == null) { // Second check inside the lock
realImage = new HighResolutionImage(filename);
}
}
}
realImage.display();
}
Q10: How does the Proxy Pattern relate to middleware?
Middleware in web frameworks (Express, Django, ASP.NET) is the Proxy Pattern applied to HTTP request handling. Each middleware component wraps the next one, can intercept the request before passing it along, and can modify the response on the way back. The "request pipeline" is a chain of proxies around the final handler.
# Django middleware — each one is a proxy around the next handler
class LoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response # "real subject"
def __call__(self, request):
print(f"[Log] {request.method} {request.path}")
response = self.get_response(request) # Delegate
print(f"[Log] Response: {response.status_code}")
return response
Key Takeaways
🎯 Core Concept: The Proxy Pattern places an intermediary in front of a real object, implementing the same interface so clients can use proxy and real object interchangeably. The proxy controls access to the real object and can add behavior (caching, logging, auth, lazy init) without the client or the real object knowing.
🔑 Key Benefits:
- Separation of concerns: Cross-cutting logic (caching, security, logging) lives in proxies, keeping the real subject clean
- Open/closed principle: Add new behaviors (a new proxy type) without modifying the real subject
- Transparency: Clients work through the subject interface and don't need to know whether they're talking to a proxy or the real thing
- Composability: Multiple proxy types can be chained to apply several concerns at once
- Testability: Each proxy can be tested independently by mocking the real subject
🌐 Language-Specific Highlights:
- Python: Use ABCs to enforce interface implementation; use
threading.Lockwith double-checked locking for thread-safe lazy init;__getattr__enables dynamic proxies - TypeScript: Always use
async/awaitconsistently across the proxy chain; type the proxy with the interface, notany - Java: Use
ConcurrentHashMapandsynchronizedfor thread safety;java.lang.reflect.Proxyfor dynamic proxies on interfaces; CGLIB/ByteBuddy for class-based dynamic proxies - JavaScript: Native
Proxyis a language feature (different from the design pattern); explicit class-based proxies are clearer for structural design; avoid cachingnullresults - C#: Use
recordtypes and immutable models; threadCancellationTokenthrough every async proxy method; consider DispatchProxy for dynamic proxies - PHP: Always type-hint interfaces, not concrete classes, to keep proxies swappable
- Go: Hold an interface reference in the proxy, not an embedded struct; always propagate errors; use
sync.RWMutexfor concurrent caches - Rust: Use
Box<dyn Trait>for owned proxy subjects;Arc<Mutex<dyn Trait>>for shared/threaded scenarios; avoid holding&mutreferences - Dart: Use
implements, notextends, for proxy classes; Dart's??=operator simplifies lazy init - Swift: Use
class(notstruct) for proxies with mutable lazy state; use protocols for the subject interface - Kotlin: Use
bydelegation to avoid boilerplate — override only the methods that need proxy logic; uselazyfor Virtual Proxies
📋 When to Use Proxy:
- You need lazy initialization for an expensive object and don't want clients to know
- You need access control without modifying the real object or its callers
- You need to add caching, logging, retry, or rate limiting as a cross-cutting concern
- You're representing an object that lives in another process or machine (Remote Proxy)
- You want to implement a circuit breaker, circuit limiter, or health monitor around a service
⚠️ When NOT to Use Proxy:
- The subject interface is very large — implementing it in every proxy becomes a maintenance burden (use delegation, dynamic proxies, or AOP instead)
- The behavior you're adding is tightly coupled to the business logic — inline it rather than creating a separate proxy
- You need to extend or modify behavior rather than control access — use Decorator instead
- The overhead of indirection outweighs the benefit for trivially cheap operations
🛠️ Best Practices Across Languages:
- Always implement the full interface: Every method must be delegated; partial proxies cause
NotImplementedExceptionorAttributeErrorsurprises - Keep proxies single-responsibility: One proxy = one concern (caching, logging, or auth — not all three)
- Chain proxies for multiple concerns: Compose proxy chains in a factory function to keep client code clean
- Make the factory the single assembly point: Build the entire proxy chain in one place so configuration is centralized
- Thread-safe lazy initialization: Always use double-checked locking or language idioms (
lazyin Kotlin,once.Doin Go) for thread safety - Don't cache null or error results: Cache only successful responses to avoid poisoning the cache
- Propagate errors faithfully: A proxy must not swallow exceptions from the real subject
- Test each proxy layer independently: Mock the inner subject to verify proxy logic in isolation
💡 Common Pitfalls:
- Incomplete interface implementation: A proxy that doesn't implement all methods breaks Liskov Substitution Principle
- Proxy doing too much: A proxy that adds validation, transformation, and caching is a service, not a proxy
- Thread-unsafe lazy init: Two threads creating the real subject simultaneously wastes resources or causes bugs
- Caching failures: Caching null/error responses means valid future requests always fail until cache expires
- Coupling to concrete types: Type-hinting concrete classes in client code prevents proxies from being injected
- Breaking the chain: Using
instanceofchecks or downcasting in client code to get the "real" implementation defeats the purpose
🎁 Advanced Techniques:
- Dynamic Proxy: Use language-native dynamic proxies (Java
Proxy, Python__getattr__, JSProxy, C#DispatchProxy) for automatic interception without per-method boilerplate - AOP (Aspect-Oriented Programming): Frameworks like Spring AOP and PostSharp generate proxies from annotations/attributes, eliminating manual proxy classes entirely
- Proxy + Decorator combo: Use Proxy to control access and Decorator to add behavior — stack them in the right order
- Circuit Breaker Proxy: Automatically open/close based on downstream failure rate — the production-grade resilience pattern
- Proxy chains with middleware: HTTP middleware pipelines (Django, Express, ASP.NET) are a chain of request proxies — the same pattern at the framework level
The Proxy Pattern is one of the most widely used patterns in production systems, often invisibly. Every time you use an ORM, call a gRPC service, use Spring's @Cacheable, or write Express middleware — you're already using Proxy. Understanding the pattern explicitly lets you apply it deliberately: to keep your domain logic clean, compose cross-cutting behaviors without pollution, and build systems that can evolve one concern at a time.
Want to dive deeper into design patterns? Check out our comprehensive guides on:
- Facade Pattern
- Flyweight Pattern
- Decorator Pattern
- Adapter Pattern
- Chain of Responsibility Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.