Proxy Design Pattern in Java with Real-World Example
Design patterns are reusable solutions to common software design problems. In my previous posts, I’ve covered patterns like Factory Method, Builder, and Chain of Responsibility. Today, I’ll explain the Proxy Pattern, another important Gang of Four (GoF) structural design pattern.
Github link to the example 👉 https://github.com/Prasadct/proxy-pattern
What is the Proxy Pattern?
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It’s like having a representative who acts on behalf of the real object. This pattern is useful when:
- You want to control access to an object
- You need to add functionality when accessing an object
- You want to delay the creation and initialization of an expensive object until it’s actually needed
Types of Proxies
There are several types of proxies, each serving different purposes:
- Virtual Proxy: Creates expensive objects on demand
- Protection Proxy: Controls access to the original object’s methods
- Remote Proxy: Represents an object that exists in a different address space
- Logging Proxy: Keeps a log of method calls to the object
- Caching Proxy: Stores results of expensive operations for reuse
Structure of the Proxy Pattern
The Proxy pattern consists of these components:
- Subject Interface: Defines the common interface for RealSubject and Proxy
- RealSubject: The real object that the proxy represents
- Proxy: Maintains a reference to the RealSubject and controls access to it
Here’s a diagram showing the structure:
Real-World Example: API Rate Limiter Proxy
Let’s implement a real-world example of the Proxy pattern: an API rate limiter. This is a common requirement in web applications to prevent abuse and ensure fair usage.
Imagine you have a weather service that provides weather data. Without rate limiting, users could potentially overwhelm your service with requests. A rate limiter proxy can control how many requests a user can make within a given time period.
Step 1: Define the Subject Interface
First, let’s define our subject interface for the weather service:
// Subject Interface
public interface WeatherService {
String getWeatherData(String location) throws Exception;
}
Step 2: Implement the Real Subject
Next, we’ll implement the actual weather service:
// Real Subject
public class RealWeatherService implements WeatherService {
@Override
public String getWeatherData(String location) throws Exception {
// In a real application, this would call an external API
System.out.println("Fetching weather data for " + location + " from external API");
// Simulate API call with a delay
Thread.sleep(1000);
// Return mock weather data
return "Temperature: 25°C, Condition: Sunny, Location: " + location;
}
}
Step 3: Implement the Proxy
Now, let’s create our rate limiter proxy:
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
// Proxy
public class RateLimitedWeatherServiceProxy implements WeatherService {
private final WeatherService realWeatherService;
private final int maxRequestsPerMinute;
private final Map<String, UserRequestInfo> userRequestsMap;
public RateLimitedWeatherServiceProxy(WeatherService realWeatherService, int maxRequestsPerMinute) {
this.realWeatherService = realWeatherService;
this.maxRequestsPerMinute = maxRequestsPerMinute;
this.userRequestsMap = new HashMap<>();
}
@Override
public String getWeatherData(String location) throws Exception {
String userId = getCurrentUserId(); // In a real app, this would be from authentication
if (isRateLimitExceeded(userId)) {
throw new Exception("Rate limit exceeded. Try again later.");
}
// If rate limit is not exceeded, delegate to the real service
return realWeatherService.getWeatherData(location);
}
private boolean isRateLimitExceeded(String userId) {
LocalDateTime currentTime = LocalDateTime.now();
// If this is a new user or the tracking info has expired, create new tracking info
if (!userRequestsMap.containsKey(userId) ||
userRequestsMap.get(userId).getWindowStartTime().plusMinutes(1).isBefore(currentTime)) {
userRequestsMap.put(userId, new UserRequestInfo(currentTime));
return false;
}
// Get existing user info and increment request count
UserRequestInfo userInfo = userRequestsMap.get(userId);
userInfo.incrementRequestCount();
// Check if the user has exceeded the rate limit
return userInfo.getRequestCount() > maxRequestsPerMinute;
}
private String getCurrentUserId() {
// In a real application, this would get the authenticated user's ID
// For this example, we'll just return a mock ID
return "user-123";
}
// Helper class to track user requests
private static class UserRequestInfo {
private final LocalDateTime windowStartTime;
private int requestCount;
public UserRequestInfo(LocalDateTime windowStartTime) {
this.windowStartTime = windowStartTime;
this.requestCount = 1; // First request
}
public void incrementRequestCount() {
requestCount++;
}
public int getRequestCount() {
return requestCount;
}
public LocalDateTime getWindowStartTime() {
return windowStartTime;
}
}
}
Step 4: Client Code Using the Proxy
Here’s how a client would use our rate-limited weather service:
// Client code
public class WeatherApp {
public static void main(String[] args) {
// Create the real service
WeatherService realService = new RealWeatherService();
// Create the proxy with a limit of 3 requests per minute
WeatherService proxy = new RateLimitedWeatherServiceProxy(realService, 3);
try {
// Try making several requests
System.out.println("First request: " + proxy.getWeatherData("Bangalore"));
System.out.println("Second request: " + proxy.getWeatherData("Mumbai"));
System.out.println("Third request: " + proxy.getWeatherData("Delhi"));
// This request should trigger the rate limit
System.out.println("Fourth request: " + proxy.getWeatherData("Chennai"));
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}
The output would look something like:
Fetching weather data for Bangalore from external API
First request: Temperature: 25°C, Condition: Sunny, Location: Bangalore
Fetching weather data for Mumbai from external API
Second request: Temperature: 25°C, Condition: Sunny, Location: Mumbai
Fetching weather data for Delhi from external API
Third request: Temperature: 25°C, Condition: Sunny, Location: Delhi
Error: Rate limit exceeded. Try again later.
Sequence of Events
Let’s look at a sequence diagram to understand how the Proxy pattern works in our example:
Another Example: Image Loading Proxy
Another classic example of the Proxy pattern is lazy loading of heavy resources like images. Let’s quickly look at a Virtual Proxy implementation for image loading:
// Subject Interface
public interface Image {
void display();
}
// Real Subject
public class RealImage implements Image {
private final String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading image: " + filename);
// Simulate heavy image loading
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}
// Proxy
public class ProxyImage implements Image {
private final String filename;
private RealImage realImage;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
// Load the image only when needed
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
// Client code
public class ImageViewer {
public static void main(String[] args) {
// Create an array of images
Image[] images = new Image[3];
images[0] = new ProxyImage("image1.jpg");
images[1] = new ProxyImage("image2.jpg");
images[2] = new ProxyImage("image3.jpg");
// Images are not loaded until display() is called
System.out.println("Images will be loaded on demand...");
// Load and display the first image
System.out.println("\nDisplaying first image:");
images[0].display();
// Display the first image again (already loaded)
System.out.println("\nDisplaying first image again:");
images[0].display();
// Load and display the second image
System.out.println("\nDisplaying second image:");
images[1].display();
}
}
Output:
Images will be loaded on demand...
Displaying first image:
Loading image: image1.jpg
Displaying image: image1.jpg
Displaying first image again:
Displaying image: image1.jpg
Displaying second image:
Loading image: image2.jpg
Displaying image: image2.jpg
When to Use the Proxy Pattern
Use the Proxy pattern when:
- You need lazy initialization (Virtual Proxy)
- You need access control to an object (Protection Proxy)
- You need to add logging, caching, or other functionality when accessing an object
- You want to control resources that are expensive to create or use
Advantages of the Proxy Pattern
Separation of Concerns: The proxy handles additional functionality while the real subject focuses on its primary responsibility
Open/Closed Principle: You can introduce new proxies without changing the real subject
Protection: Controls access to the real subject
Improved Performance: Can implement caching or lazy loading to optimize resource usage
Disadvantages of the Proxy Pattern
Increased Complexity: Adds another layer of indirection
Response Time: May increase response time in some cases
Implementation Overhead: Requires creating additional classes
Proxy Pattern vs. Other Patterns
Decorator vs. Proxy: Both wrap an object, but decorators add responsibilities while proxies control access
Adapter vs. Proxy: Adapters change an interface, while proxies keep the same interface
Facade vs. Proxy: Facades simplify a complex system, while proxies control access to objects
Conclusion
The Proxy pattern is a powerful tool for controlling access to objects. It can help you implement various functionalities like lazy loading, caching, access control, and logging without modifying the original object. In our real-world examples, we saw how proxies can be used for rate limiting API calls and lazy loading images.
As with all design patterns, use the Proxy pattern when it clearly solves your specific problem, not just because it’s available. Consider the performance implications and added complexity before implementing this pattern in your code.
Have you used the Proxy pattern in your projects? Share your experiences in the comments below!