Exploring The Proxy Design Pattern: Implementing Lazy Loading and Method Pre-Authorization

Leandro Luque
6 min readNov 21, 2023

--

Have you ever wondered how lazy loading and method pre-authorization are implemented? This article explores the Proxy design pattern, a versatile tool in a developer’s arsenal, which can be used for these purposes.

Image generated with the Bing AI image creator.

Understanding the basics with a real-world scenario

Let’s start with a common scenario: retrieving Order objects from a relational database. Consider the following classes and relationships:

Code for the classes Order, Item, and Product.

If you’re not familiar with the @OneToMany and @ManyToOne annotations, they are part of JPA (Java Persistence API) and inform a persistence library (e.g. Hibernate) how to handle relationships between classes. In our example, they represent one-to-many and many-to-one relationships, respectively.

When generating SQL to retrieve Order data from the database, we have the option to fetch both Item and Product data in a single query or to fetch only a portion of it. Retrieving more data than necessary can lead to inefficient use of processing power and memory. Let’s assume that Product data isn’t always needed. In this scenario, we opt to load Product data only when the program explicitly requests it, as seen in operations like item.getProduct().getName() (it's important to be mindful of the N+1 problem that can arise in such situations. It will be covered in a future article). This means the initial SQL query will fetch only Order and Item data, while Product data will be retrieved from the database only upon request.

This approach is referred to as lazy loading, a strategy that contrasts with eager loading. In eager loading, all related data is retrieved from the database in a single comprehensive query whenever feasible. In the context of JPA, this behavior can be controlled using the fetch attribute. For instance, the relationship to Product can be specified as @ManyToOne(…, fetch = FetchType.LAZY), indicating that the Product data should be loaded lazily.

Implementing Lazy Loading

How do we implement this without altering the existing Product class? There are two main approaches.

One approach involves extending the Product class and, within each method, verifying if the product's data has already been loaded from the database. If it has, the method simply calls the corresponding method of the superclass (using super). If not, it first fetches the necessary data from the database. This can be implemented as follows.

An example of lazy loading using inheritance.

Another approach, particularly effective when an interface is available, involves implementing the same interface that Product implements and using delegation to invoke methods, rather than relying on inheritance.

An example of lazy loading using association.

The latter approach is often more advantageous as it enables the addition of behavior to an existing object, a feature not achievable with inheritance. To illustrate, if there exists a partially filled Product object, it can be passed as an argument to the class in the second approach. However, this would not be feasible with the first approach, which relies on inheritance.

This concept of augmenting an object’s behavior is known as the Proxy design pattern. It is a structural design pattern introduced in the seminal “Design Patterns: Elements of Reusable Object-Oriented Software” by the Gang of Four (GoF). In the context of our example, a traditional UML-based representation of this pattern would be as follows.

UML class diagram illustrating the Proxy design pattern.
UML sequence diagram illustrating a proxy call.

As illustrated in the diagrams, the Client interacts with the ProductProxy, which in turn delegates execution to the original class. This delegation occurs either before or after the ProductProxy adds some additional functionality.

Creating a class that delegates its methods to another object can be quite code-intensive. While IDEs, plugins and tools like Lombok offer some assistance, maintaining such a class can still be challenging. It’s crucial to remember that any changes in the original class necessitate corresponding updates in the proxy class, adding to the maintenance burden.

Java offers a solution to simplify this process through the use of dynamic proxies. To leverage this feature, one needs to create a class that implements the InvocationHandler interface. This approach streamlines the creation and maintenance of proxy classes, making it a more efficient alternative to manually coding all the method delegations.

Code for a proxy that logs the running time of a method.

Let's break down what each part of the proxy creation code does:

  1. Proxy Creation: Proxy.newProxyInstance(...) is a method provided by Java's java.lang.reflect.Proxy class. It is used to dynamically create a proxy object that implements a specified set of interfaces. In this case, it's creating a proxy for the IProduct interface.
  2. ClassLoader: The first argument, Product.class.getClassLoader(), specifies the class loader that will define the proxy class. In this context, it's using the class loader of the Product class, which ensures that the proxy class is defined in the same namespace as the Product class.
  3. Interface Array: The second argument, new Class[] { IProduct.class }, is an array of interfaces that the proxy class will implement. Here, the proxy will implement the IProduct interface. This means the proxy will have all the methods declared in the IProduct interface.
  4. Invocation Handler: The third argument, new ExecutionTimeLoggerProxy(originalProduct), is an instance of InvocationHandler. This handler intercepts method calls made on the proxy object. In this case, ExecutionTimeLoggerProxy is a custom handler designed to measure and log the execution time of each method call on the originalProduct, which is an instance of the Product class.

Now, whenever a method of the Product class is called through its proxy, the call is routed through our custom InvocationHandler implementation. This setup allows for additional operations, like logging or authorization checks, to be performed alongside the original method functionality.

The Proxy class in Java provides other two useful methods:

  • isProxyClass(Class<?> cl): This static method returns true if the specified class is a proxy class created by the Proxy class.
  • getInvocationHandler(Object proxy): This static method returns the invocation handler associated with the proxy instance. If the object passed is not a proxy instance, it throws an IllegalArgumentException.

Returning to lazy loading, we can apply a similar approach. When fetching OrderItem from the database, we can fill the Product field with a proxy that loads data on demand.

Code for a proxy that illustrates the idea of lazy loading data.

As you can see, dynamic proxy creation in Java requires the use of interfaces. However, when working with libraries such as Hibernate, the necessity to define interfaces for this purpose is eliminated. Hibernate employs bytecode manipulation libraries (e.g. CGLIB, Byte Buddy, and Javassist) to generate subclass proxies of entities. These proxies extend the original entity classes and override their methods, thereby seamlessly integrating lazy loading behavior.

Securing Methods with Proxies

In the context of securing methods, as seen in frameworks like Spring Security, proxies prove to be highly effective when interfaces are available. For instance, by utilizing the @PreAuthorize annotation, we can construct a proxy that verifies user permissions prior to the execution of a method. This approach allows for the enforcement of security constraints at the method level, ensuring that only authorized users can invoke certain operations

Code for a method that requires pre-authorization.
Code for a proxy that illustrates the idea of pre-authorization.

Conclusion

The Proxy design pattern is a powerful tool. By understanding and implementing this pattern, developers can modify the behavior of objects when necessary. Remember to use it judiciously, keeping in mind the maintenance and performance implications.

Thank you for exploring this subject with me. If you want to learn more about Java and system architecture design, please follow me.

As a Technology Manager @ NewGo Technology, an esteemed international software consultancy, I am part of a dynamic environment that actively encourages its employees to engage in knowledge sharing with the broader community. I extend my sincere gratitude to NewGo for their support in facilitating such valuable publications.

--

--

Leandro Luque

Tech manager @ NewGo Tecnologia & Associate Professor @ Fatec | Full Stack Engineer | Java, NodeJS, React & RN | PhD @ USP