Proxy Design Pattern in OOPS

What is Proxy Pattern?

In object-oriented programming, Proxy is a structural design pattern that provides a placeholder for another object to control its access. Here proxy object works as an intermediary between the client and the real object so that the client will interact with the proxy instead of the real object. 

In this situation, the proxy object can perform tasks like managing access to the real object and providing additional functionalities like filtering requests, caching, logging, etc.

Let’s understand proxy pattern via an analogy

In many situations, organizations use a representative to interact with other parties on their behalf. For example, suppose a company (client) that wants to negotiate a contract with a supplier (real object). Instead of sending a large team to meet with the supplier, the company can hire a single representative to negotiate on their behalf. Here representative acts as a proxy for the company, who represent their interests and communicate with the supplier to reach an agreement.

Key components and structure

UML diagram of proxy design pattern

  • RealService object: This object does most of the real work and the proxy object controls access to it.
  • Service interface: Both Proxy and RealService implement the Service interface. This allows any client to treat the proxy just like the RealService.
  • Proxy object: Proxy instantiates or handles the creation of the RealService object. For this, Proxy keeps a reference to the Service, so it can forward requests to the RealService.
  • Client: An user who wants to use the RealService object.

Let’s understand proxy pattern via an example

In the code below, we have created a Video class that loads and plays a video file. If we observe, we are loading the video file when we are creating an object of a Video class, even if the user doesn’t want to play it. So if a video file is large, the process of loading a video file from disk will be a slow process. This can result in a poor user experience, and it may not be an optimal solution.

Here is a Java example code without proxy pattern.

public class Video {
    private String fileName;
    
    public Video(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }
    
    public void play() {
        System.out.println("Playing video: " + fileName);
    }
    
    private void loadFromDisk(String fileName) {
        System.out.println("Loading video: " + fileName);
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        Video video = new Video("video.mp4");
        video.play();
    }
}

So what would be the optimal solution? How do we use a proxy pattern to solve this problem efficiently? One idea is: We should not load the video file until we request to play it. Let’s understand this solution using proxy pattern.

proxy design pattern real world example

  • We create an interface Video which is implemented by VideoImpl and VideoProxy. VideoProxy class will act as a proxy for the VideoImpl class.
  • Now user will use an object of VideoProxy. Here VideoProxy class will only create an instance of VideoImpl class when the user requests to play the video by calling the play method of VideoProxy. This is lazy initialization! This approach will improve user experience because we only load the video file when the user wants to play it.

Here is the updated java code using proxy pattern:

interface Video {
    void play();
}

class VideoImpl implements Video {
    private String fileName;
    
    public VideoImpl(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }
    
    public void play() {
        System.out.println("Playing video: " + fileName);
    }
    
    private void loadFromDisk(String fileName) {
        System.out.println("Loading video: " + fileName);
    }
}

class VideoProxy implements Video {
    private String fileName;
    private Video video;
    
    public VideoProxy(String fileName) {
        this.fileName = fileName;
    }
    
    public void play() {
        if (video == null) {
            video = new VideoImpl(fileName);
        }
        video.play();
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        Video video = new VideoProxy("video.mp4");
        video.play();
    }
}

Why do we use proxy pattern?

  • To add an extra layer of security and abstraction between the client and the actual object: In certain scenarios, we may want to restrict access to the object. So we can use a proxy as a gatekeeper, which can allow or deny access to the object based on the given restriction.
  • To cache results from an object to improve performance: When a client requests a resource, proxy will check if the resource has already been cached. If yes, then proxy will return the cached resource instead of calling the actual object.
  • To instantiate real objects when it is needed (lazy loading): This could improve performance and reduce memory usage. In other words, this can be useful in situations where we want to defer the creation of an object until it is actually needed.
  • To provide remote access and separation of concern: A proxy pattern can be used to provide a local representation of a remote object and hide the complexity of remote communication. This separation of concerns allows for better maintainability and reusability of code.
  • The proxy pattern is also very useful for testing. You can create proxy objects for all the dependencies in a class so that you can focus on testing the behaviour of a particular class instead of testing its dependencies as well

Different types of proxies

  1. Remote Proxy is used when objects are located in a different address space, such as a remote server. In this case, the proxy object serves as a local representative for the remote object.
  2. Virtual Proxy is used to create expensive objects on demand. In this scenario, the proxy creates a virtual representation of the real object and only creates the real object when it is actually needed.
  3. Protection Proxy is used to control access to an object. In this case, the proxy checks if the client has the necessary permissions to access the real object and only allows access if the client is authorized.
  4. Smart Proxy is used to add additional functionality to an object, such as caching (storing the results for reuse in the future) or logging (logging the use of an object for debugging purposes).

A sample use case of proxy pattern

Problem: Suppose we have a user service that returns a list of users from a database, but the service is slow and we want to improve its performance. Here is a constraint: We cannot modify the user service code directly.

Solution: We can use the proxy design pattern to create a new class that acts as a proxy for the user service. This proxy will intercept client requests to retrieve the user list and return a cached version if it exists. 

Here’s an example implementation:

// Interface for the user service
public interface UserService {
    List<User> getUsers();
}

// Implementation of the user service
class UserServiceImpl implements UserService {
    public List<User> getUsers() {
        // Code to retrieve user list from database
    }
}

// Proxy implementation of the user service
public class UserServiceProxy implements UserService {
    private UserService userService;
    private List<User> cachedUsers;

    public List<User> getUsers() {
        if (userService == null) {
            userService = new UserServiceImpl();
            cachedUsers = userService.getUsers();
        }

        return cachedUsers;
    }
}

// Example usage of the proxy implementation
public class Main {
    public static void main(String[] args) {
        UserService proxy = new UserServiceProxy();

        // First call retrieves the user list from the real service and caches it
        List<User> users = proxy.getUsers();

        // Subsequent calls retrieve the cached user list
        List<User> cachedUsers = proxy.getUsers();
    }
}

In the above example, UserServiceImpl class is the real implementation of the UserService interface, which contains the code to retrieve the user list from the database. The UserServiceProxy class is a proxy and it intercepts calls to getUsers().

If the user list has not already been retrieved, it delegates the call to the UserServiceImpl implementation and caches the result. After this, subsequent calls to getUsers() return the cached user list instead of going to the real service. This can improve performance by a huge margin.

Best practices for implementing proxy pattern

  • We should use an interface or abstract class to define an interface between the real object and the proxy. This will help to provide a consistent interface to the client and make it easier to swap out different types of proxies based on demand.
  • Proxy can add extra latency and overhead. So the best idea would be to use lazy loading to defer the creation or retrieval of the real object.
  • We should use a proper caching strategy to improve performance.
  • For easier maintenance and debugging, we should avoid unnecessary functionalities and keep the proxy code as simple as possible.
  • We should use dependency injection to manage the creation and configuration of proxies. This can help to reduce coupling.
  • We should use appropriate error handling and logging strategies to diagnose and troubleshoot issues that may arise with the proxy.

Relation of proxy pattern with other design patterns

The proxy pattern can be combined with other patterns or techniques to create more complex systems. Here are a few examples:

Adapter Pattern: A proxy can act as an adapter between a client and a real object, allowing the client to use the real object in a way that it wasn’t originally designed for. This is similar to how the Adapter Pattern allows objects with incompatible interfaces to work together.

Decorator Pattern: Decorator and Proxy have different purposes but similar structures. A proxy can act as a decorator by adding behavior to the real object, either before or after it is called. This is similar to how the Decorator Pattern adds behavior to an object at runtime.

Factory pattern: The factory pattern can be used to create instances of a proxy object dynamically, based on the needs of the system. This can be useful when you want to create different types of proxy objects depending on the context or configuration of the system.

We can use asynchronous programming techniques (callbacks or promises) with proxies to improve performance and responsiveness. Here we can use a proxy to represent a long-running or resource-intensive operation and return control to the client code immediately, while the operation continues in the background. When the operation is complete, the proxy will notify the client code and return the result.

Please write in the message below if you find anything incorrect, or if you want to share more insight. Enjoy learning, OOPS!

Share Your Insights

☆ 16-week live DSA course
☆ 16-week live ML course
☆ 10-week live DSA course

More from EnjoyAlgorithms

Self-paced Courses and Blogs

Coding Interview

Machine Learning

System Design

Our Newsletter

Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.