Proxy Pattern: Control Object Access in OOP
TL;DR
The Proxy Pattern provides a placeholder or surrogate object that controls access to another object. It acts as an intermediary, allowing you to add functionality like lazy initialization, access control, logging, or caching without modifying the original object.
Core Concept
What is the Proxy Pattern?
The Proxy Pattern is a structural design pattern that provides a substitute or placeholder for another object. The proxy controls access to the original object, allowing you to perform operations before or after the request reaches the real object.
Think of a proxy like a representative or agent. When you hire a lawyer, they act as your proxy in legal matters — they represent you, but they can also add value by filtering requests, providing advice, or handling paperwork before matters reach you.
Why Use a Proxy?
Proxies solve several common problems:
- Lazy Initialization (Virtual Proxy): Delay creating expensive objects until they’re actually needed
- Access Control (Protection Proxy): Add authentication or authorization checks before allowing operations
- Remote Access (Remote Proxy): Represent objects in different address spaces (different machines, processes)
- Logging/Monitoring (Logging Proxy): Track operations performed on the real object
- Caching (Caching Proxy): Store results of expensive operations for reuse
Structure
The pattern involves three key components:
- Subject Interface: Defines the common interface for both RealSubject and Proxy
- RealSubject: The actual object that does the real work
- Proxy: Maintains a reference to RealSubject and controls access to it
The proxy implements the same interface as the real subject, making them interchangeable from the client’s perspective. This is crucial — clients shouldn’t know whether they’re working with a proxy or the real object.
When to Use
Consider the Proxy Pattern when:
- You need to control access to an object
- You want to add functionality without modifying the original class
- Object creation is expensive and should be deferred
- You need to track or log object usage
- You’re working with remote objects or resources
Visual Guide
Proxy Pattern Structure
classDiagram
class Subject {
<<interface>>
+request()
}
class RealSubject {
+request()
}
class Proxy {
-realSubject: RealSubject
+request()
}
class Client
Subject <|.. RealSubject
Subject <|.. Proxy
Proxy o-- RealSubject
Client --> Subject
The Proxy and RealSubject both implement the Subject interface. The Proxy holds a reference to RealSubject and delegates requests to it, potentially adding extra behavior.
Proxy Request Flow
sequenceDiagram
participant Client
participant Proxy
participant RealSubject
Client->>Proxy: request()
Note over Proxy: Pre-processing<br/>(auth, logging, etc.)
Proxy->>RealSubject: request()
RealSubject-->>Proxy: result
Note over Proxy: Post-processing<br/>(caching, cleanup)
Proxy-->>Client: result
The proxy intercepts client requests, performs additional operations, delegates to the real subject, and can process the response before returning it.
Examples
Example 1: Virtual Proxy (Lazy Loading)
A virtual proxy delays the creation of an expensive object until it’s actually needed. This is common with large images, database connections, or heavy computations.
from abc import ABC, abstractmethod
import time
class Image(ABC):
"""Subject interface"""
@abstractmethod
def display(self):
pass
class RealImage(Image):
"""RealSubject - expensive to create"""
def __init__(self, filename):
self.filename = filename
self._load_from_disk()
def _load_from_disk(self):
print(f"Loading image from disk: {self.filename}")
time.sleep(2) # Simulate expensive loading
def display(self):
print(f"Displaying {self.filename}")
class ImageProxy(Image):
"""Proxy - delays creation until needed"""
def __init__(self, filename):
self.filename = filename
self._real_image = None # Not created yet!
def display(self):
# Lazy initialization: create only when needed
if self._real_image is None:
self._real_image = RealImage(self.filename)
self._real_image.display()
# Client code
print("Creating proxy...")
image = ImageProxy("large_photo.jpg")
print("Proxy created (image not loaded yet)\n")
print("First display call:")
image.display()
print()
print("Second display call:")
image.display()
Expected Output:
Creating proxy...
Proxy created (image not loaded yet)
First display call:
Loading image from disk: large_photo.jpg
Displaying large_photo.jpg
Second display call:
Displaying large_photo.jpg
Key Points:
- The proxy is created instantly without loading the image
- The real image loads only on first
display()call - Subsequent calls use the already-loaded image
- Client code doesn’t know it’s using a proxy
Try it yourself: Add a get_size() method that also triggers lazy loading. How would you handle multiple methods that need the real object?
Example 2: Protection Proxy (Access Control)
A protection proxy controls access based on permissions or authentication.
from abc import ABC, abstractmethod
from typing import Optional
class BankAccount(ABC):
"""Subject interface"""
@abstractmethod
def withdraw(self, amount: float) -> bool:
pass
@abstractmethod
def get_balance(self) -> float:
pass
class RealBankAccount(BankAccount):
"""RealSubject - actual bank account"""
def __init__(self, balance: float):
self._balance = balance
def withdraw(self, amount: float) -> bool:
if amount <= self._balance:
self._balance -= amount
print(f"Withdrew ${amount}. New balance: ${self._balance}")
return True
print(f"Insufficient funds. Balance: ${self._balance}")
return False
def get_balance(self) -> float:
return self._balance
class BankAccountProxy(BankAccount):
"""Protection Proxy - adds authentication"""
def __init__(self, real_account: RealBankAccount, password: str):
self._real_account = real_account
self._password = password
self._authenticated = False
def authenticate(self, password: str) -> bool:
if password == self._password:
self._authenticated = True
print("Authentication successful")
return True
print("Authentication failed")
return False
def withdraw(self, amount: float) -> bool:
if not self._authenticated:
print("Access denied: Please authenticate first")
return False
print(f"Authorization check passed for withdrawal of ${amount}")
return self._real_account.withdraw(amount)
def get_balance(self) -> float:
if not self._authenticated:
print("Access denied: Please authenticate first")
return 0.0
return self._real_account.get_balance()
# Client code
real_account = RealBankAccount(1000.0)
proxy = BankAccountProxy(real_account, "secret123")
print("Attempting withdrawal without authentication:")
proxy.withdraw(100)
print()
print("Authenticating with wrong password:")
proxy.authenticate("wrong")
print()
print("Authenticating with correct password:")
proxy.authenticate("secret123")
print()
print("Attempting withdrawal after authentication:")
proxy.withdraw(100)
print()
print("Checking balance:")
balance = proxy.get_balance()
print(f"Current balance: ${balance}")
Expected Output:
Attempting withdrawal without authentication:
Access denied: Please authenticate first
Authenticating with wrong password:
Authentication failed
Authenticating with correct password:
Authentication successful
Attempting withdrawal after authentication:
Authorization check passed for withdrawal of $100
Withdrew $100. New balance: $900.0
Checking balance:
Current balance: $900.0
Key Points:
- Proxy enforces authentication before delegating to real account
- All methods check authentication state
- Real account logic remains unchanged
- Security concerns are separated from business logic
Try it yourself: Add role-based access control where only “admin” users can withdraw more than $500.
Example 3: Caching Proxy
A caching proxy stores results of expensive operations to avoid redundant work.
from abc import ABC, abstractmethod
import time
from typing import Dict
class DataService(ABC):
"""Subject interface"""
@abstractmethod
def fetch_user_data(self, user_id: int) -> dict:
pass
class RealDataService(DataService):
"""RealSubject - makes expensive API calls"""
def fetch_user_data(self, user_id: int) -> dict:
print(f"Fetching data for user {user_id} from database...")
time.sleep(1) # Simulate network delay
return {
"id": user_id,
"name": f"User{user_id}",
"email": f"user{user_id}@example.com"
}
class CachingDataServiceProxy(DataService):
"""Caching Proxy - stores results"""
def __init__(self, real_service: DataService):
self._real_service = real_service
self._cache: Dict[int, dict] = {}
def fetch_user_data(self, user_id: int) -> dict:
if user_id in self._cache:
print(f"Returning cached data for user {user_id}")
return self._cache[user_id]
print(f"Cache miss for user {user_id}")
data = self._real_service.fetch_user_data(user_id)
self._cache[user_id] = data
return data
def clear_cache(self):
"""Additional proxy-specific method"""
self._cache.clear()
print("Cache cleared")
# Client code
real_service = RealDataService()
proxy = CachingDataServiceProxy(real_service)
print("First request for user 1:")
data1 = proxy.fetch_user_data(1)
print(f"Data: {data1}\n")
print("Second request for user 1 (should be cached):")
data1_again = proxy.fetch_user_data(1)
print(f"Data: {data1_again}\n")
print("Request for user 2:")
data2 = proxy.fetch_user_data(2)
print(f"Data: {data2}\n")
print("Request for user 1 again:")
data1_third = proxy.fetch_user_data(1)
print(f"Data: {data1_third}")
Expected Output:
First request for user 1:
Cache miss for user 1
Fetching data for user 1 from database...
Data: {'id': 1, 'name': 'User1', 'email': 'user1@example.com'}
Second request for user 1 (should be cached):
Returning cached data for user 1
Data: {'id': 1, 'name': 'User1', 'email': 'user1@example.com'}
Request for user 2:
Cache miss for user 2
Fetching data for user 2 from database...
Data: {'id': 2, 'name': 'User2', 'email': 'user2@example.com'}
Request for user 1 again:
Returning cached data for user 1
Data: {'id': 1, 'name': 'User1', 'email': 'user1@example.com'}
Key Points:
- First request is slow (1 second delay)
- Subsequent requests for same user are instant
- Cache is transparent to the client
- Proxy can have additional methods like
clear_cache()
Java/C++ Notes:
- In Java, use
HashMap<Integer, UserData>for the cache - In C++, use
std::unordered_map<int, UserData>and smart pointers (std::shared_ptr) for the real service reference - Both languages benefit from explicit interface definitions
Try it yourself: Add a time-to-live (TTL) feature where cached data expires after 5 seconds.
Common Mistakes
1. Breaking the Interface Contract
Mistake: The proxy doesn’t implement the same interface as the real subject, or adds required parameters that the real subject doesn’t have.
# WRONG - Proxy has different interface
class ImageProxy:
def display(self, force_reload=False): # Extra parameter!
pass
class RealImage:
def display(self): # Different signature
pass
Why it’s wrong: Clients can’t use proxy and real subject interchangeably. This defeats the purpose of the pattern.
Fix: Both must implement the same interface exactly. Add proxy-specific methods separately if needed.
# CORRECT
class Image(ABC):
@abstractmethod
def display(self):
pass
class ImageProxy(Image):
def display(self):
# Implementation
pass
def force_reload(self): # Separate method for proxy-specific behavior
pass
2. Forgetting to Delegate
Mistake: The proxy performs operations itself instead of delegating to the real subject.
# WRONG - Proxy doesn't delegate
class BankAccountProxy:
def __init__(self, real_account):
self._real_account = real_account
self._balance = 1000 # Proxy maintains its own state!
def withdraw(self, amount):
if self._authenticated:
self._balance -= amount # Wrong: not delegating
Why it’s wrong: The proxy duplicates logic and state, leading to inconsistencies. The real subject and proxy can get out of sync.
Fix: Proxy should only add cross-cutting concerns (auth, logging, caching) and delegate actual work.
# CORRECT
class BankAccountProxy:
def withdraw(self, amount):
if self._authenticated:
return self._real_account.withdraw(amount) # Delegate!
3. Creating the Real Object Too Early
Mistake: In a virtual proxy, creating the real object in the proxy’s constructor defeats lazy initialization.
# WRONG - Defeats lazy loading
class ImageProxy:
def __init__(self, filename):
self._real_image = RealImage(filename) # Created immediately!
Why it’s wrong: The expensive object is created even if never used. You lose the performance benefit.
Fix: Create the real object only when first needed.
# CORRECT
class ImageProxy:
def __init__(self, filename):
self.filename = filename
self._real_image = None # Not created yet
def display(self):
if self._real_image is None:
self._real_image = RealImage(self.filename) # Lazy creation
self._real_image.display()
4. Not Handling All Methods
Mistake: Implementing proxy logic for some methods but forgetting others in the interface.
# WRONG - Incomplete proxy
class BankAccountProxy(BankAccount):
def withdraw(self, amount):
if self._authenticated:
return self._real_account.withdraw(amount)
def get_balance(self):
# Forgot to check authentication!
return self._real_account.get_balance()
Why it’s wrong: Security holes or inconsistent behavior. Some operations bypass proxy logic.
Fix: Apply proxy logic consistently to all interface methods.
5. Exposing the Real Subject
Mistake: Providing direct access to the real subject, allowing clients to bypass the proxy.
# WRONG - Exposes real subject
class ImageProxy:
def get_real_image(self):
return self._real_image # Clients can bypass proxy!
Why it’s wrong: Clients can circumvent access control, caching, or other proxy functionality.
Fix: Keep the real subject private. Never expose it through public methods.
Interview Tips
What Interviewers Look For
1. Recognize When to Use Proxies
Interviewers often present scenarios and ask you to choose a pattern. Be ready to identify proxy use cases:
- “How would you implement lazy loading for large images?”
- “Design a system to control access to sensitive operations”
- “How would you add caching without modifying existing code?”
Strong answer: Identify the specific proxy type (virtual, protection, caching) and explain why it fits. Mention that the proxy maintains the same interface as the real subject.
2. Explain the Difference from Similar Patterns
You’ll likely be asked: “How is Proxy different from Decorator or Adapter?”
Key distinctions:
- Proxy vs Decorator: Proxy controls access and manages the lifecycle of the real object. Decorator adds new functionality. Proxy often creates/destroys the real subject; decorator wraps an existing object.
- Proxy vs Adapter: Proxy has the same interface as the real subject. Adapter converts one interface to another.
- Proxy vs Facade: Proxy represents a single object with the same interface. Facade provides a simplified interface to a complex subsystem.
Interview tip: Draw a quick diagram showing the relationships. Mention that proxy and decorator have similar structures but different intents.
3. Discuss Trade-offs
Be prepared to discuss disadvantages:
- Adds an extra layer of indirection (slight performance overhead)
- Increases code complexity
- Can make debugging harder (which object is actually being called?)
Strong answer: “While proxies add a layer of indirection, the benefits usually outweigh the costs. For example, lazy loading can save significant memory, and the performance cost of one extra method call is negligible compared to loading a 10MB image.”
4. Code It Correctly
If asked to implement a proxy:
- Start with the interface/abstract class
- Implement the real subject
- Implement the proxy with the same interface
- Show the proxy delegating to the real subject
- Add the specific proxy behavior (caching, auth, etc.)
Common interview question: “Implement a caching proxy for a database query service.”
Approach:
- Define the service interface
- Create a real service that “queries the database” (simulate with sleep)
- Create a proxy that checks a cache dictionary before delegating
- Demonstrate with example usage
5. Real-World Examples
Mention concrete examples to show practical knowledge:
- Virtual proxies: ORM frameworks (Hibernate, SQLAlchemy) use lazy loading for related objects
- Protection proxies: Spring Security uses proxies to enforce method-level security
- Remote proxies: RPC frameworks, REST clients, database connection pools
- Smart references: Python’s
weakrefproxy, C++ smart pointers
Interview tip: If you’ve used any of these technologies, mention it: “In my last project, we used SQLAlchemy’s lazy loading, which is essentially a virtual proxy pattern.”
6. Handle Follow-up Questions
Be ready for deeper questions:
- “How would you implement a thread-safe caching proxy?” (Answer: Use locks or thread-safe collections)
- “What if the real object creation fails?” (Answer: Proxy should handle exceptions and possibly retry)
- “How do you test proxies?” (Answer: Mock the real subject, verify proxy behavior separately)
Red Flags to Avoid:
- Saying proxy and decorator are “basically the same”
- Implementing a proxy that doesn’t match the real subject’s interface
- Not being able to name a specific proxy type (virtual, protection, remote, etc.)
- Forgetting to delegate to the real subject
Key Takeaways
-
The Proxy Pattern provides a surrogate that controls access to another object while maintaining the same interface, enabling lazy initialization, access control, caching, or logging without modifying the real subject.
-
Proxy and real subject must implement the same interface to be interchangeable from the client’s perspective. The proxy delegates actual work to the real subject after performing its own operations.
-
Four main proxy types solve different problems: Virtual proxies delay expensive object creation, protection proxies enforce access control, remote proxies represent objects in different address spaces, and caching proxies store results to avoid redundant operations.
-
Proxies add a layer of indirection which introduces slight overhead but provides significant benefits like improved performance (lazy loading, caching), enhanced security (access control), and better separation of concerns.
-
Distinguish proxies from similar patterns: Unlike decorators (which add functionality to existing objects), proxies control access and manage object lifecycle. Unlike adapters (which change interfaces), proxies maintain the same interface as the real subject.