GoF Design Patterns – Proxy Design Pattern

Overview

The Proxy design pattern is a structural design pattern that provides a substitute or a placeholder for the original object. The proxy wraps the original object, and each access to the original object always goes through the proxy. Since the original object and the proxy implement the same interface, the client is unable to distinguish between the original object and the proxy. The proxy allows you to implement some additional logic before and after the original method is executed. It is often used to implement cross-cutting concerns like additional logging or security.

Proxy usually adds before and after logic that is executed around the actual method that is invoked. Proxy delegates actual method execution to the object that is being proxied. Implementing both before and after logic is not mandatory. Some proxies might implement only one of them.

A proxy hides the actual object, keeps a reference to it, and the client invokes the actual object through the proxy:

Here is the sequence of operations involved with actual object execution via proxy:

Use Cases

  1. Security Access Control proxies: When an object needs to be protected from unauthorized access, a protection proxy can be used to control access to the object. The protection proxy can check the credentials of the client before allowing access to the object.
  2. Lazy Initialization Virtual proxies: When an object is expensive to create or initialize, a lazy initializing virtual proxy can be used to create a lightweight representation of the object. The virtual proxy can defer the creation of the real object until it is actually needed, thus improving performance.
  3. Logging Proxy: When we need additional logging whenever the method on the original object is executed, Logging Proxy should be considered. In this approach, the client interacts with Logging Proxy, which pretends to be the original object. Logging Proxy logs each method invocation, often on the tracing logging level. This can be useful for production support purposes.
  4. Caching Proxy: When method results is expensive to get, and at the same time, method results can be cached, Caching Proxy should be considered. The idea here is to wrap the original object within a proxy object that will save the result of method invocation when the method is invoked for the first time. When the method is invoked for the second time, pre-fetched results are taken from the cache.
    • Warning – cache usage always increases the complexity, as sooner or later, the cache needs to be properly invalidated. Failing to properly invalidate the cache is often a source of hard-to-trace bugs.
  5. Remote proxies: When an object is located in a remote location and needs to be accessed over the network, a remote proxy can be used to provide a local representation of the object, which is responsible for hiding all details related to network communication with an object located on another machine. Using this approach, an object that is located in a remote location is used as it would be a local object.
    • Warning – although this approach does simplify the code by hiding all network communication details, it can lead to performance or service availability issues. Using an object through the network is always different than using a local object within the same machine / jvm / memory. In-memory calls are very different when compared to over-the-network calls. With the network, you need to consider latency and possible communication errors. Using the Remote Proxy approach, it is easy to forget about this and treat this object as a local object. When viewing a remote object as a local object, we can forget that those are actually network calls, and a call to each method might not be cheap, which in the end, will increase the chattiness of the application. The same applies to availability. When communicating with a remote object, we often need to embed communication error handling into the business logic flow. Logic, like start releasing the inventory when the call to the payment system fails, needs to be possible. If the proxy would hide error details and would only implement simple retries, having this logic would not be possible.

Structure

The Proxy design pattern consists of several components:

  • Service Interface: The interface that defines the operations exposed by the service being proxied. The real service and the proxy will share and implement operations from this interface.
  • Real Service: The real object that is being proxied.
  • Proxy: The object that acts as a substitute for the real service. The proxy has the same interface as the real service and delegates requests to the real service. The proxy allows adding before and after additional logic whenever a method on the real service is executed.

Here’s a UML diagram that illustrates the structure of the Proxy design pattern:

Example Code

In the below example, we will use the proxy design pattern to implement a security proxy for an object that will model bank account operations.

Additionally, we will have an authentication token that holds granted authorities. The security proxy will verify bank account operation eligibility based on authorities in the authentication token.

In the example, we will use the following components:

  1. Service Interface: will be implemented by BankAccount interface. This interface will expose the following operations: deposit, withdraw, getBalance.
  2. Real Service: Will be implemented by BankAccountImpl. This object will provide logic for the following operations: deposit, withdraw, getBalance.
  3. Proxy: Will be implemented by BankAccountSecurityProxy. This proxy will provide security cross-cutting concerns. It will check the eligibility of the following operations: deposit, withdraw, getBalance.

Let’s start by implementing BankAccount interface:

Java
public interface BankAccount {
    void deposit(Money amount);

    void withdraw(Money amount);

    Money getBalance();
}

Now, we will implement real bank account operation under BankAccountImpl:

Java
public class BankAccountImpl implements BankAccount {
    private Money balance = Money.of(0, "USD");

    @Override
    public void deposit(Money amount) {
        balance = balance.add(amount);
    }

    @Override
    public void withdraw(Money amount) {
        if (balance.isGreaterThanOrEqualTo(amount))
            balance = balance.subtract(amount);
        else
            throw new IllegalStateException("Unable to withdraw amount " + amount
                    + " because amount exceeds current balance " + balance);
    }

    @Override
    public Money getBalance() {
        return balance;
    }
}

Prior implementing the security proxy, we will implement the other required objects:

Java
public record AuthenticationToken(String name, Set<Authority> authorities) {
    public boolean hasAuthority(Authority authority) {
        return authorities.contains(authority);
    }
}

public enum Authority {
    ALLOWED_DEPOSIT,
    ALLOWED_WITHDRAW,
    ALLOWED_GET_BALANCE,
}

Now, we can implement BankAccountSecurityProxy that will check eligibility of each bank operation, based on AuthenticationToken and Authority:

Java
public class BankAccountSecurityProxy implements BankAccount {
    private final BankAccount bankAccount;
    private final AuthenticationToken authenticationToken;

    public BankAccountSecurityProxy(BankAccount bankAccount, AuthenticationToken authenticationToken) {
        this.bankAccount = bankAccount;
        this.authenticationToken = authenticationToken;
    }

    @Override
    public void deposit(Money amount) {
        checkIfAllowedToDeposit();
        bankAccount.deposit(amount);
    }

    @Override
    public void withdraw(Money amount) {
        checkIfAllowedToWithdraw();
        bankAccount.withdraw(amount);
    }

    @Override
    public Money getBalance() {
        checkIfAllowedGetBalance();
        return bankAccount.getBalance();
    }

    private void checkIfAllowedToDeposit() {
        if (!authenticationToken.hasAuthority(ALLOWED_DEPOSIT))
            throw new IllegalStateException("User is not allowed to deposit money using token " + authenticationToken);
    }

    private void checkIfAllowedToWithdraw() {
        if (!authenticationToken.hasAuthority(ALLOWED_WITHDRAW))
            throw new IllegalStateException("User is not allowed to withdraw money using token " + authenticationToken);
    }

    private void checkIfAllowedGetBalance() {
        if (!authenticationToken.hasAuthority(ALLOWED_GET_BALANCE))
            throw new IllegalStateException("User is not allowed to get balance using token " + authenticationToken);
    }
}

Notice, how the above code implements the same interface as the real bank account object, but prior delegating the operation to the real bank account object, it executes checkIfAllowedToDeposit, checkIfAllowedToWithdraw, checkIfAllowedGetBalance first.

Now, we can use the above code as following:

Java
AuthenticationToken authenticationToken = new AuthenticationToken("john", Set.of(ALLOWED_DEPOSIT, ALLOWED_GET_BALANCE));

BankAccount bankAccount = new BankAccountSecurityProxy(
        new BankAccountImpl(),
        authenticationToken
);

System.out.println("Depositing money...");
bankAccount.deposit(Money.of(500, "USD"));
bankAccount.deposit(Money.of(300, "USD"));

System.out.println("Getting current balance...");
Money currentBalance = bankAccount.getBalance();
System.out.println("Current balance = " + currentBalance);

System.out.println("Withdrawing money...");
bankAccount.withdraw(Money.of(100, "USD"));

The above code will produce the following output:

Java
Depositing money...
Getting current balance...
Current balance = USD 800.00
Withdrawing money...
Exception in thread "main" java.lang.IllegalStateException: User is not allowed to withdraw money using token AuthenticationToken[name=john, authorities=[ALLOWED_GET_BALANCE, ALLOWED_DEPOSIT]]

Notice how BankAccountSecurityProxy throws an exception on withdraw operation. This is because used AuthenticationToken does not have ALLOWED_WITHDRAW authority assigned.

You can find the source code for this example on GitHub.

Alternatives

The Proxy design pattern can be implemented manually, like in the example above, however, you should also consider the following alternatives:

  1. JDK Dynamic Proxy
  2. CGLIB Proxy using CGLIB Enhancer
  3. Aspect-oriented programming with AspectJ
  4. Aspect Oriented Programming with Spring

Summary

The Proxy design pattern is a structural pattern that provides an object that acts as a substitute for the original object by wrapping it. The proxy and the original object implement the same interface, and each access to the original object goes through the proxy. The proxy enables adding extra logic before and after executing the original method, typically for implementing cross-cutting concerns like logging, caching, or security. The pattern consists of the following key components: Service Interface, Real Service, and the Proxy itself.

References