Proxy Pattern: Controlling Access Like a Middleman
Sometimes, direct access to an object isn’t desirable or possible. You might want to add an extra layer of control — like validation, caching, lazy loading, or logging — without changing the object’s actual implementation.
This is where the Proxy Pattern becomes valuable.
What is the Proxy Pattern?
The Proxy Pattern provides a surrogate or placeholder for another object to control access to it.
In simpler terms:
- You have a real object that does the work.
- A proxy object stands in front of it and adds logic like access control, caching, etc.
Real-World Analogy: Apartment Security
When a visitor comes to an apartment:
- They don’t directly enter the resident’s unit.
- Instead, they go through security, which may:
- Check identity
- Record the visit
- Grant or deny access
The security desk is the proxy between the visitor and the resident.
Real Backend Example: Caching with a Proxy
Let’s build an example where we have a class that fetches data from an API or database, and a proxy that caches the response.
Step 1: Define the Interface
from abc import ABC, abstractmethod class DataFetcher(ABC): @abstractmethod def fetch_data(self, key: str) -> dict: pass
Step 2: Real Implementation (e.g., API call or DB access)
import time class RealDataFetcher(DataFetcher): def fetch_data(self, key: str) -> dict: print(f"Fetching data for '{key}' from API...") time.sleep(1) # simulate network/database latency return {"key": key, "value": f"Data for {key}"}
Step 3: Proxy Implementation with Caching
class CachingProxyDataFetcher(DataFetcher): def __init__(self, real_fetcher: DataFetcher): self.real_fetcher = real_fetcher self.cache = {} def fetch_data(self, key: str) -> dict: if key in self.cache: print(f"Returning cached data for '{key}'") return self.cache[key] print(f"No cache for '{key}'. Using real fetcher...") data = self.real_fetcher.fetch_data(key) self.cache[key] = data return data
Step 4: Using the Proxy
real_fetcher = RealDataFetcher() proxy_fetcher = CachingProxyDataFetcher(real_fetcher) # First call — goes through real fetcher print(proxy_fetcher.fetch_data("user:123")) # Second call — uses cache print(proxy_fetcher.fetch_data("user:123")) # Another key — triggers real fetcher again print(proxy_fetcher.fetch_data("user:456"))
Output:
No cache for 'user:123'. Using real fetcher... Fetching data for 'user:123' from API... {'key': 'user:123', 'value': 'Data for user:123'} Returning cached data for 'user:123' {'key': 'user:123', 'value': 'Data for user:123'} No cache for 'user:456'. Using real fetcher... Fetching data for 'user:456' from API... {'key': 'user:456', 'value': 'Data for user:456'}
Types of Proxies
| Type | Purpose |
|---|---|
| Virtual Proxy | Lazy loading (e.g., loading large objects only when needed) |
| Protection Proxy | Access control or authorization checks |
| Remote Proxy | Access objects in a different address space (e.g., gRPC, REST) |
| Smart Proxy | Adds logging, caching, or other logic |
Why Use the Proxy Pattern?
- Add features without changing the real object (Open/Closed Principle)
- Control access, enforce permissions, or audit usage
- Improve performance via caching or lazy loading
- Secure sensitive operations
When to Use It in Backend Development?
- Add caching around expensive operations (API, DB, ML model)
- Secure access to internal systems (RBAC, token-based access)
- Lazy loading large datasets or configuration files
- Throttle or rate-limit API calls
Gotchas
- Be careful of stale caches or side effects in proxies
- Adds complexity — don't over-engineer if not needed
- Too many proxies can make debugging difficult
Closing Thoughts
The Proxy Pattern is incredibly practical in the backend world. Whether you’re caching results, controlling access, or logging usage, proxies help you extend functionality without modifying core components — cleanly and safely.