The Proxy Design Pattern is a structural pattern that lets you provide a surrogate or placeholder for another object. A proxy controls access to the original object, allowing you to perform additional logic before or after the request gets to the original object.
Think of it like a security checkpoint. Instead of everyone having direct access to a secure facility (the Real Subject), they must first go through a guard (the Proxy). The guard can check credentials, log visitors, or deny access altogether, all without changing the facility itself.
When Do You Need a Proxy?
The Proxy pattern is incredibly versatile. It helps organize the relationships between objects and can solve several common problems in software design.
Here’s a summary of its primary use cases:
mindmap
root((Proxy Pattern Use Cases))
Performance
Caching
::icon(fa fa-database)
Store results of expensive operations.
"Lazy Initialization (Virtual Proxy)"
::icon(fa fa-hourglass-half)
Delay creation of heavy objects.
Security
"Access Control (Protection Proxy)"
::icon(fa fa-lock)
Guard access to sensitive objects.
Resilience
"Retry Logic (Remote Proxy)"
::icon(fa fa-retweet)
Handle network failures for remote services.
Circuit Breaker
::icon(fa fa-bolt)
Prevent repeated calls to a failing service.
Monitoring
Logging & Auditing
::icon(fa fa-file-alt)
Track interactions with the real object.
In this article, we’ll build a practical example of a Caching Proxy to optimize a slow video downloader.
The Problem: Redundant, Expensive Operations
Imagine you have a RealVideoDownloader class that downloads a video from a URL. This operation is slow and resource-intensive.
# The "Real Subject"
import time
class RealVideoDownloader:
def download_video(self, url: str) -> str:
print(f"Connecting to {url}...")
print("Downloading video...")
time.sleep(5) # Simulate a slow network operation
video_data = f"Video data from {url}"
print("Download complete.")
return video_data
Now, if your application requests the same video multiple times, it will download it from scratch every single time. This is a huge waste of time and resources.
# Client code
downloader = RealVideoDownloader()
print("--- First Request ---")
downloader.download_video("https://example.com/video1.mp4")
print("\n--- Second Request (Same Video) ---")
downloader.download_video("https://example.com/video1.mp4") # Wastes another 5 seconds!
This is where a Caching Proxy comes to the rescue.
The Solution: A Caching Proxy
A Caching Proxy sits between the client and the RealVideoDownloader. When a request comes in, the proxy first checks its internal cache.
- Cache Hit: If the video is already in the cache, the proxy returns it instantly without bothering the
RealVideoDownloader. - Cache Miss: If the video is not in the cache, the proxy delegates the request to the
RealVideoDownloader, downloads the video, saves it to the cache, and then returns it to the client.
This flow ensures that each unique video is downloaded only once.
sequenceDiagram
participant C as Client
participant P as CachedDownloaderProxy
participant R as RealVideoDownloader
C->>P: download_video("video1.mp4")
P->>P: Check cache for "video1.mp4"
Note right of P: Cache Miss!
P->>R: download_video("video1.mp4")
R-->>P: Returns video data
P->>P: Save video data to cache
P-->>C: Returns video data
C->>P: download_video("video1.mp4")
P->>P: Check cache for "video1.mp4"
Note right of P: Cache Hit!
P-->>C: Returns video data from cache
Implementing the Proxy Pattern in Python
Let’s refactor our code to use the Proxy pattern. A good practice is to structure our components into separate files.
caching_proxy_example/
├── main.py
└── downloaders/
├── __init__.py
├── subject.py
├── real_subject.py
└── proxy.py
Step 1: Define the Common Interface (The Subject)
Both the Real Subject and the Proxy must implement the same interface so the client can treat them interchangeably. We’ll use Python’s abc module for this.
What’s an Interface? An interface is like a contract. It defines what methods a class should have, but not how they should be implemented. This allows us to swap out different implementations (like swapping the
RealVideoDownloaderfor ourCachedDownloaderProxy) without changing the client code.
# downloaders/subject.py
from abc import ABC, abstractmethod
class IVideoDownloader(ABC):
"""The Subject interface declares common operations for both RealSubject and Proxy."""
@abstractmethod
def download_video(self, url: str) -> str:
pass
Step 2: Implement the Real Subject
This is our original, slow class. It now inherits from the IVideoDownloader interface.
# downloaders/real_subject.py
import time
from .subject import IVideoDownloader
class RealVideoDownloader(IVideoDownloader):
"""The RealSubject contains the core, resource-intensive business logic."""
def download_video(self, url: str) -> str:
print(f"Connecting to {url}...")
print("Downloading video from the internet...")
time.sleep(5) # Simulate slow network
video_data = f"VideoData({url})"
print("Download complete.")
return video_data
Step 3: Create the Caching Proxy
The Proxy also implements the IVideoDownloader interface. It holds a reference to the RealVideoDownloader and manages the cache.
# downloaders/proxy.py
from .subject import IVideoDownloader
from .real_subject import RealVideoDownloader
class CachedDownloaderProxy(IVideoDownloader):
"""
The Proxy controls access to the RealSubject and implements caching.
"""
def __init__(self) -> None:
self._real_downloader = RealVideoDownloader()
self._cache: dict[str, str] = {}
def download_video(self, url: str) -> str:
if url not in self._cache:
print("Proxy: 'Cache miss. Delegating to real downloader.'")
# Download the video using the real subject
video_data = self._real_downloader.download_video(url)
# Store the result in the cache
self._cache[url] = video_data
print("Proxy: 'Storing result in cache.'")
else:
print("Proxy: 'Cache hit! Serving video from cache.'")
return self._cache[url]
[!TIP] Lazy Initialization: Notice the
_real_downloaderis created in the__init__method. For very heavy objects, you could delay its creation untildownload_videois called for the first time. This is known as a Virtual Proxy.
Step 4: Update the Client Code
The final step is to update the client to use the proxy instead of the real subject. The beauty of this pattern is that no other client code needs to change, as they both share the same download_video method.
# main.py
from downloaders.real_subject import RealVideoDownloader
from downloaders.proxy import CachedDownloaderProxy
from downloaders.subject import IVideoDownloader
- downloader: IVideoDownloader = RealVideoDownloader()
+ downloader: IVideoDownloader = CachedDownloaderProxy()
print("--- First Request ---")
downloader.download_video("https://example.com/video1.mp4")
print("\n--- Second Request (Same Video) ---")
downloader.download_video("https://example.com/video1.mp4") # Now it's instant!
print("\n--- Third Request (New Video) ---")
downloader.download_video("https://example.com/video2.mp4")
When you run the updated main.py, the first request for video1.mp4 will be slow. But the second request will be served instantly from the cache!
Other Types of Proxies
While we focused on a Caching Proxy, the pattern is powerful enough for other scenarios:
- Protection Proxy: Checks if the client has the required permissions to execute a request. Ideal for managing user roles (
adminvs.guest). - Logging Proxy: Intercepts requests to log them for analytics or auditing purposes before passing them to the real subject.
- Remote Proxy: Provides a local representation of an object that lives in a different address space (e.g., on a remote server). It handles all the complex networking details.
Best Practices and Final Thoughts
- Interface is Key: The power of the Proxy pattern comes from the shared interface. It allows the proxy to be a perfect stand-in for the real object.
- Manage Cache Size: An unbounded cache can consume all available memory. In a production system, you’d use a cache with an eviction policy, like LRU (Least Recently Used).
- Thread Safety: If your application is multi-threaded, your proxy’s cache might need locks to prevent race conditions where multiple threads try to write to the cache simultaneously.
The Proxy pattern is a fantastic tool for adding behavior to objects without modifying their source code, leading to cleaner, more maintainable, and more efficient systems.
classDiagram
direction LR
class IVideoDownloader {
<<interface>>
+download_video(url) str
}
class RealVideoDownloader {
+download_video(url) str
}
class CachedDownloaderProxy {
- _real_downloader: RealVideoDownloader
- _cache: dict
+download_video(url) str
}
class Client
IVideoDownloader <|-- RealVideoDownloader
IVideoDownloader <|-- CachedDownloaderProxy
Client ..> IVideoDownloader : uses
CachedDownloaderProxy ..> RealVideoDownloader : delegates to