The Proxy Design Pattern is a structural pattern that lets you provide a substitute or placeholder for another object. But why would you want to do that? 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 guard in front of a building. You can’t just walk into the main office (the Real Object). You first have to talk to the guard (the Proxy). The guard might check your ID, log your visit time, or deny you access altogether. The guard controls access and adds extra functionality without changing the office itself.
Let’s visualize this relationship:
graph TD
subgraph "Client's Perspective"
Client
end
subgraph "Service Layer"
Proxy -- intercepts --> RealObject
end
Client -- interacts with --> Proxy
style RealObject fill:#f9f,stroke:#333,stroke-width:2px
style Proxy fill:#bbf,stroke:#333,stroke-width:2px
The client thinks it’s talking to the real object, but it’s actually interacting with the proxy. This seamless substitution is the core strength of the pattern.
Why Do We Need the Proxy Pattern?
The Proxy pattern is incredibly versatile. It’s not just for one specific problem but is a solution to a whole category of them. Here are its most common applications:
mindmap
root((Proxy Use Cases))
Access Control
::icon(fa fa-shield-alt)
Protects the real object from unauthorized clients.
Example: A user proxy that checks permissions before allowing access to sensitive admin functions.
Lazy Initialization (Virtual Proxy)
::icon(fa fa-hourglass-half)
Delays the creation of a resource-intensive object until it's truly needed.
Example: An image proxy that only loads a high-resolution image when the user clicks on it.
Caching (Caching Proxy)
::icon(fa fa-database)
Stores the results of expensive operations and serves them from a cache to improve performance.
Example: Our video downloader example below!
Logging & Auditing
::icon(fa fa-file-alt)
Logs requests and responses before and after they reach the real object.
Example: A database proxy that logs every SQL query executed.
Failure Protection (Protection Proxy)
::icon(fa fa-broadcast-tower)
Can provide fallback behavior, like retrying a failed network request or implementing a circuit breaker.
Example: An API proxy that retries a request if the remote server is temporarily unavailable.
Case Study: Building a High-Performance Caching Video Downloader
Let’s make this concrete. Imagine we have a service that downloads videos from the internet. A direct implementation might be slow if users repeatedly request the same video.
The Initial Problem: No Caching
First, let’s define a common interface for our downloaders.
[!TIP] Using an interface is crucial. It ensures that the client, the real object, and the proxy all conform to the same contract, making them interchangeable. This is known as “programming to an interface.”
Here’s our project structure:
src/main/java/com/example/
├── service/
│ ├── VideoDownloader.java // Interface
│ ├── RealVideoDownloader.java // Real Subject
│ └── CachingVideoProxy.java // Proxy
└── Main.java // Client
VideoDownloader.java (The Interface)
package com.example.service;
// The common interface for both the Real Subject and the Proxy
public interface VideoDownloader {
String downloadVideo(String url);
}
RealVideoDownloader.java (The Real Subject)
This class does the “heavy lifting” of downloading a video from a URL. We’ll simulate the delay.
package com.example.service;
// The "heavy" object that does the real work
public class RealVideoDownloader implements VideoDownloader {
@Override
public String downloadVideo(String url) {
System.out.println("Connecting to internet and downloading video from: " + url);
// Simulate a network delay
try {
Thread.sleep(4000); // 4-second delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Download interrupted.");
}
System.out.println("-> Download complete for: " + url);
return "VideoData[" + url + "]";
}
}
Main.java (The Client)
The client directly uses RealVideoDownloader. Notice the performance hit when we request the same video twice.
package com.example;
import com.example.service.RealVideoDownloader;
import com.example.service.VideoDownloader;
public class Main {
public static void main(String[] args) {
VideoDownloader downloader = new RealVideoDownloader();
System.out.println("--- First Request ---");
downloader.downloadVideo("https://example.com/video1.mp4"); // Takes 4s
System.out.println("\n--- Second Request (Same Video) ---");
downloader.downloadVideo("https://example.com/video1.mp4"); // Takes another 4s!
System.out.println("\n--- Third Request ---");
downloader.downloadVideo("https://example.com/video2.mp4"); // Takes 4s
}
}
Each download takes 4 seconds, even for the same URL. We can do much better!
The Solution: A Caching Proxy
Now, let’s introduce a CachingVideoProxy. This proxy will sit between the client and the RealVideoDownloader. It will maintain a cache of already downloaded videos.
Here’s the class structure we’re aiming for:
classDiagram
direction LR
class Client
class VideoDownloader {
<<interface>>
+downloadVideo(url) string
}
class RealVideoDownloader {
+downloadVideo(url) string
}
class CachingVideoProxy {
-realDownloader: RealVideoDownloader
-videoCache: Map<String, String>
+downloadVideo(url) string
}
Client ..> VideoDownloader : uses
VideoDownloader <|.. RealVideoDownloader : implements
VideoDownloader <|.. CachingVideoProxy : implements
CachingVideoProxy o-- RealVideoDownloader : has a
CachingVideoProxy.java (The Proxy)
This class implements the same VideoDownloader interface but adds the caching logic.
package com.example.service;
import java.util.HashMap;
import java.util.Map;
// The Proxy object that adds caching
public class CachingVideoProxy implements VideoDownloader {
private final RealVideoDownloader realDownloader;
private final Map<String, String> videoCache = new HashMap<>();
public CachingVideoProxy() {
// The proxy creates and manages the real object
this.realDownloader = new RealVideoDownloader();
}
@Override
public String downloadVideo(String url) {
// 1. Check the cache first
if (videoCache.containsKey(url)) {
System.out.println("Found in cache! Serving video from: " + url);
return videoCache.get(url);
}
// 2. If not in cache, delegate to the real downloader
System.out.println("Not in cache. Delegating to real downloader...");
String videoData = realDownloader.downloadVideo(url);
// 3. Store the result in the cache for future requests
videoCache.put(url, videoData);
System.out.println("-> Stored in cache: " + url);
return videoData;
}
}
The flow of a request now looks like this:
sequenceDiagram
participant Client
participant Proxy as CachingVideoProxy
participant Real as RealVideoDownloader
Client->>Proxy: downloadVideo("video1.mp4")
Proxy->>Proxy: Is "video1.mp4" in cache? (No)
Proxy->>Real: downloadVideo("video1.mp4")
Note over Real: (Simulates 4s delay)
Real-->>Proxy: returns VideoData
Proxy->>Proxy: Store "video1.mp4" in cache
Proxy-->>Client: returns VideoData
Client->>Proxy: downloadVideo("video1.mp4")
Proxy->>Proxy: Is "video1.mp4" in cache? (Yes)
Note over Proxy: (Returns immediately)
Proxy-->>Client: returns VideoData from cache
Refactoring the Client to Use the Proxy
The final step is to update our client. The change is minimal—we just instantiate CachingVideoProxy instead of RealVideoDownloader.
- import com.example.service.RealVideoDownloader;
+ import com.example.service.CachingVideoProxy;
import com.example.service.VideoDownloader;
public class Main {
public static void main(String[] args) {
- VideoDownloader downloader = new RealVideoDownloader();
+ VideoDownloader downloader = new CachingVideoProxy();
System.out.println("--- First Request ---");
downloader.downloadVideo("https://example.com/video1.mp4"); // Takes 4s
System.out.println("\n--- Second Request (Same Video) ---");
downloader.downloadVideo("https://example.com/video1.mp4"); // Instant!
System.out.println("\n--- Third Request ---");
downloader.downloadVideo("https://example.com/video2.mp4"); // Takes 4s
}
}
Now, the second request for video1.mp4 is served instantly from the cache, dramatically improving performance. The client code barely changed, but the system’s behavior is significantly better.
Deep Dive: Other Types of Proxies
Best Practices and Potential Pitfalls
[!TIP] Keep Proxies Focused: A proxy should ideally handle one responsibility (e.g., caching or access control). If you need both, you can chain proxies together (a client talks to a caching proxy, which talks to a security proxy, which talks to the real object).
[!WARNING] Cache Invalidation is Hard: In our caching example, what if the video at
video1.mp4is updated? Our cache will serve the old, stale version. Real-world caching proxies need a strategy for cache invalidation (e.g., time-to-live (TTL) timers) to keep data fresh.
Conclusion
The Proxy pattern is a powerful tool for managing object interactions. By inserting a surrogate object, you can add behavior like caching, security, and lazy loading without the client or the real object ever knowing. It allows you to build systems that are more performant, secure, and robust, all while keeping your code clean and decoupled.