Static Proxy

In software engineering, the Proxy Design Pattern is a structural pattern used to provide a surrogate or placeholder for another object. One of its most straightforward implementations is the Static Proxy.

At its core, a static proxy allows you to control access to a "real" object and inject additional behavior—such as logging, security checks, or performance monitoring—without modifying the original object's source code. The term "static" refers to the fact that the proxy class is explicitly written by the developer and its relationship with the target object is determined at compile-time.

The Architecture of a Static Proxy

To implement a static proxy, four distinct roles must interact within the system:

  • Subject (Interface): This is the contract that defines the available operations. Both the real object and the proxy must implement this interface to ensure they are interchangeable from the client's perspective.
  • RealSubject (The Target): This class contains the actual business logic. It is the "source of truth" that performs the core tasks the application requires.
  • Proxy: This class implements the same Subject interface. It maintains a private reference to the RealSubject. When a method is called on the proxy, it executes "pre-processing" logic, delegates the call to the real object, and then executes "post-processing" logic.
  • Client: The end-user of the service. Crucially, the client interacts only with the Subject interface, meaning it remains unaware of whether it is talking to the real object or the proxy.

Implementation Workflow

Building a static proxy follows a disciplined five-step process:

  1. Define the Interface: Establish the common ground (the Subject) that both the proxy and the real object will follow.
  2. Implement the Real Logic: Create the RealSubject class that fulfills the interface requirements.
  3. Create the Proxy Class: Implement the same interface in a new class. This class must accept an instance of the RealSubject (usually via a constructor) to hold a reference to it.
  4. Inject Cross-Cutting Concerns: Inside the proxy's methods, wrap the call to the real object with the desired additional logic (e.g., timing, logging, or validation).
  5. Client Orchestration: The client instantiates the real object, passes it to the proxy, and then performs all operations through the proxy instance.

Practical Example: User Service Management

Let's look at a Java implementation where we want to track how long it takes to save a user to a database without cluttering our business logic with timing code.

1. The Subject Interface

public interface UserService {
    void save(String username);
}

2. The RealSubject

public class UserServiceImpl implements UserService {
    @Override
    public void save(String username) {
        // Simulating business logic
        System.out.println("Saving user: " + username);
    }
}

3. The Proxy Class

public class UserServiceProxy implements UserService {
    private final UserService target;

    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    @Override
    public void save(String username) {
        long startTime = System.currentTimeMillis();
        System.out.println("[LOG] Starting save operation...");

        try {
            // Delegating the actual work to the real object
            target.save(username);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            System.out.println("[LOG] Operation completed in " + duration + "ms");
        }
    }
}

4. The Client Execution

public class Client {
    public static void main(String[] args) {
        // Initialize the real object
        UserService realService = new UserServiceImpl();
        
        // Wrap it in the proxy
        UserService proxyService = new UserServiceProxy(realService);

        // The client interacts with the proxy via the interface
        proxyService.save("Alice");
    }
}

Expected Output:

[LOG] Starting save operation...
Saving user: Alice
[LOG] Operation completed in 1ms

In this example, UserServiceImpl remains "pure"—it only knows how to save a user. The responsibility of logging and performance measurement is entirely encapsulated within UserServiceProxy.

Critical Analysis: Pros and Cons

While the static proxy is a powerful tool for adhering to the Open-Closed Principle (classes should be open for extension but closed for modification), it is not without its drawbacks.

Advantages

  • Simplicity: The logic is explicit, easy to debug, and easy for developers to follow.
  • Decoupling: It separates core business logic from "cross-cutting concerns" (logic that affects multiple parts of an application, like logging).
  • Transparency: Because the proxy implements the same interface, the client code does not need to change to accommodate the new functionality.
  • Performance: Since the proxy is determined at compile-time, there is no runtime overhead associated with reflection or dynamic bytecode generation.

Limitations

  • Class Explosion: For every new service or interface, you must manually write a corresponding proxy class. In large-scale systems, this leads to a massive increase in the number of files.
  • Maintenance Burden: If the Subject interface changes (e.g., a new method is added), you must manually update both the RealSubject and the Proxy.
  • Code Duplication: If multiple services require the same logging logic, you may end up writing very similar proxy code repeatedly.
  • Lack of Flexibility: The relationship between the proxy and the target is "hard-coded" at compile-time, making it difficult to change behaviors dynamically during program execution.

When to Use Static Proxy

Given its limitations, a static proxy is best suited for specific scenarios:

  • Small-scale Projects: When the number of classes is manageable and the overhead of managing extra files is low.
  • Performance-Critical Systems: In environments where the micro-overhead of dynamic proxies (like JDK Dynamic Proxy or CGLIB) is unacceptable.
  • Educational Purposes: It serves as the perfect stepping stone to understanding Aspect-Oriented Programming (AOP) and more advanced dynamic proxying techniques.
  • Fixed Logic: When the additional logic (like a specific security check) is unlikely to change and is strictly tied to a specific interface.

Static Proxy vs. Dynamic Proxy

To truly master the proxy pattern, one must understand how it differs from its more advanced counterpart, the Dynamic Proxy.

Feature Static Proxy Dynamic Proxy
Generation Time Compile-time (Manual) Runtime (Automatic)
Implementation Standard classes written by hand Uses Reflection or Bytecode manipulation (JDK, CGLIB)
Maintenance High (Manual updates required) Low (One handler can manage many objects)
Scalability Low (Leads to class explosion) High (Highly reusable)
Complexity Low High

Conclusion

The static proxy is a fundamental building block in software design. By intercepting calls to a target object, it allows developers to augment system behavior cleanly and effectively. While it may lack the scalability and flexibility of dynamic proxies found in modern frameworks like Spring AOP, its simplicity and performance make it an invaluable tool for targeted, high-performance logic enhancement. Understanding this pattern is essential for any developer looking to write modular, maintainable, and professional-grade code.