Proxy Design Pattern: Enhance System Performance and Security

What is Proxy design Pattern?

The Proxy Design Pattern is a structural design pattern that allows an object to act as an interface to another object. It is commonly used to control access to an object, delay the full cost of its creation and initialization until it is actually used, or provide a placeholder for an object that is expensive to create. Essentially, the proxy intercepts calls to the real object, adding a layer of control over the underlying object’s access.

When to use Proxy pattern?

The Proxy Design Pattern is particularly useful in scenarios where an object requires a surrogate or placeholder to control access to it. It is commonly used when you need to add an additional layer of complexity or control without changing the object itself. Here are real-world use cases for the pattern :

  1. Access Control: Proxies can control access to an object based on access rights. For example, in a corporate network, a proxy server can act as an intermediary between company computers and the internet, blocking access to unapproved websites and monitoring user activity.
  2. Lazy Initialization (Virtual Proxy): This type of proxy handles expensive and resource-intensive object creation by delaying it until the object is actually needed. An example of this would be the image loading in a document viewer software, where high-resolution images are only loaded on demand when the user decides to view them to save memory and processing power.
  3. Logging and Auditing: Proxies can log and audit actions performed on the real object. For instance, an application can use a proxy to monitor and log queries executed on a database, helping with performance tuning, debugging, or tracking user actions.
  4. Caching: Proxy servers are used to cache web pages and content from the internet to improve load times for users. When a web page or resource is requested, the proxy checks if it has a cached version of the content, which is faster to deliver than retrieving it from the original source each time.
  5. Protection Proxy: Similar to access control, this type of proxy protects the real object from harmful actions. For example, a software program might use a protection proxy to control write operations on an object, ensuring that no harmful data can corrupt the state of the object.
  6. Smart Reference: Proxies can perform additional actions when an object is accessed. For example, a proxy can track the number of references to an object and free the object when there are no more references, essentially adding automatic memory management to objects without built-in garbage collection.

Each of these use cases highlights how the Proxy Design Pattern can be effectively utilized to add functionality or security measures without altering the original classes or objects, making it a powerful tool in software design.

Why do we use Proxy Design Pattern?

  1. Separation of Concerns: The Proxy pattern helps decouple the client of an object from the actual object that performs the required task. This separation allows independent development and changes to the client or the service without affecting the other, adhering to a clean separation of concerns.
  2. Single Responsibility Principle: By using proxies, responsibilities can be distributed appropriately. For example, a proxy object can take over responsibilities such as managing access control, logging, or lazy initialization, while the real object focuses solely on its core functionality. Each class handles one responsibility and does it well.
  3. Interface Segregation Principle: Proxy objects can also help in adhering to the Interface Segregation Principle by acting as middle-men that expose a simpler, more specific interface to clients. This can prevent a system from forcing clients to depend on interfaces they do not use.
  4. Open/Closed Principle: Proxies support the Open/Closed Principle by allowing new functionalities to be added, like caching and logging, without changing the underlying object’s code. This makes the system open for extension but closed for modification.
  5. Liskov Substitution Principle: In many cases, proxies implement the same interfaces as their real objects, meaning they can be substituted for each other without affecting the correctness of the program. This ensures that a proxy can stand in for its real object without the client knowing the difference, which is a direct application of the Liskov Substitution Principle.
  6. Improved Security and Controlled Access: Proxies offer a way to protect the real objects by controlling access to them. This is particularly important for systems exposed to potentially harmful environments or where security is a major concern.
  7. Efficient Resource Management: Through lazy initialization and caching, proxies can significantly enhance performance by optimizing resource management. They ensure that heavy resources are only created and loaded when absolutely necessary, and once loaded, are reused efficiently.

How to use Proxy design pattern? (In Java)

Let us take an example to build a system for managing document access in a company, where documents can be of a sensitive nature, requiring audit trails for access and potentially expensive operations for document preparation. We will go over both methods of coding, first without using the proxy pattern and then using the proxy pattern.

Without using proxy pattern

Challenges in the code below:

  1. Control logic is integrated into the service class. This means more lines of code and more scenarios to consider during debugging and testing. If an issue arises or a bug needs to be fixed within the document retrieval logic, it becomes harder to isolate and address because the control code is intertwined. This can lead to longer maintenance and debugging cycles.
  2. By handling access control, logging, and document retrieval, the service class is given multiple reasons to change, which violates the SRP. If the access rules change (e.g., adding new roles or changing permissions), the service class must be modified, which also risks introducing bugs into the document retrieval logic. Similarly, changes to the logging mechanism may require adjustments in the same class that handles business-critical operations.
  3. The direct integration of control mechanisms with business logic creates tight coupling between components that ideally should be independent. If the document service needs to be replaced or upgraded, the control mechanisms also need to be re-implemented or at least heavily reviewed, leading to more work and a higher chance of errors. This coupling makes it difficult to reuse or replace individual components without affecting others.
  4. With a tightly coupled system, adding new features or scaling specific aspects like security or logging can become cumbersome. If the system needs to support more complex security features (like dynamic access controls based on user activity or context), integrating these into a monolithic service class can be very challenging and may necessitate significant refactoring. Integrated classes with multiple responsibilities are generally harder to test. Testing the document service now requires setting up more complex scenarios, including various access control contexts and logging behaviors. This can increase the complexity of test cases and require more effort to achieve comprehensive test coverage.
  5. Changes in how features like logging are implemented (for example, switching from console logging to file logging or to a cloud-based logging service) require modifications directly in the service class. Each change affects the entire class, not just the logging functionality. This can lead to potential downtime or feature instability every time a related feature is updated or modified.
public interface DocumentService {
    String getDocument(String docId, String userRole);
}

public class DocumentServiceImpl implements DocumentService {
    public String getDocument(String docId, String userRole) {
        if (!authorizeAccess(docId, userRole)) {
            throw new SecurityException("Access denied to document " + docId);
        }
        logAccess(docId, userRole);
        System.out.println("Fetching document: " + docId);
        // Assume complex operations to retrieve document
        return "Content of document " + docId;
    }

    private boolean authorizeAccess(String docId, String userRole) {
        // Implement authorization logic based on user role and document ID
        return "ADMIN".equals(userRole);
    }

    private void logAccess(String docId, String userRole) {
        System.out.println(userRole + " accessed document: " + docId);
    }
}

public class Main {
    public static void main(String[] args) {
        DocumentService docService = new DocumentServiceImpl();
        try {
            String content = docService.getDocument("doc123", "ADMIN");
            System.out.println(content);
        } catch (SecurityException e) {
            System.out.println(e.getMessage());
        }
    }
}

Using proxy pattern

It solves all the issues mentioned in the previous example that was not using the proxy design pattern

public interface DocumentService {
    String getDocument(String docId);
}

public class DocumentServiceImpl implements DocumentService {
    public String getDocument(String docId) {
        System.out.println("Fetching document: " + docId);
        // Assume complex operations to retrieve document
        return "Content of document " + docId;
    }
}


//Proxy class
public class SecureDocumentProxy implements DocumentService {
    private DocumentService docService;
    private String userRole;

    public SecureDocumentProxy(DocumentService docService, String userRole) {
        this.docService = docService;
        this.userRole = userRole;
    }

    public String getDocument(String docId) {
        if (!authorizeAccess(docId)) {
            throw new SecurityException("Access denied to document " + docId);
        }
        logAccess(docId);
        return docService.getDocument(docId);
    }

    private boolean authorizeAccess(String docId) {
        // Implement authorization logic based on user role and document ID
        return "ADMIN".equals(userRole);
    }

    private void logAccess(String docId) {
        System.out.println(userRole + " accessed document: " + docId);
    }
}

//Caller class
public class Main {
    public static void main(String[] args) {
        DocumentService realService = new DocumentServiceImpl();
        DocumentService secureDocService = new SecureDocumentProxy(realService, "ADMIN");
        try {
            String content = secureDocService.getDocument("doc123");
            System.out.println(content);
        } catch (SecurityException e) {
            System.out.println(e.getMessage());
        }
    }
}
See more in