· 8 min read ·
java software engineering

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:

  1. Virtual Proxy: Creates expensive objects on demand
  2. Protection Proxy: Controls access to the original object’s methods
  3. Remote Proxy: Represents an object that exists in a different address space
  4. Logging Proxy: Keeps a log of method calls to the object
  5. 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:

Diagram 1

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:

Diagram 2

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:

  1. You need lazy initialization (Virtual Proxy)
  2. You need access control to an object (Protection Proxy)
  3. You need to add logging, caching, or other functionality when accessing an object
  4. 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!