Java Proxy Pattern: Control Object Access with Lazy Loading, Security, and Caching

0

Proxy Pattern

Imagine you want to access a valuable painting stored inside a secure museum room. You do not walk directly into the room and touch the painting. Instead, you interact with a museum representative who checks your identity, verifies permissions, records your visit, and then allows access when appropriate.

That representative acts as a proxy.

In software, the Proxy Pattern places an object in front of another object and controls access to it. The proxy usually implements the same interface as the real object, allowing clients to interact with the proxy without knowing whether they are communicating with the real object or an intermediary.

Important: A proxy is not simply another object that calls the real object. Its main purpose is to control, enhance, delay, restrict, monitor, or otherwise manage access to the real object.

Why Do We Need the Proxy Pattern?

In a simple application, a client can directly create and use an object.

RealService service = new RealService();
service.process();

This becomes problematic when accessing the real service is expensive, sensitive, remote, or requires additional rules.

For example, suppose RealService performs an expensive operation. Every call may consume significant resources.

RealService service = new RealService();

service.process();
service.process();
service.process();

Perhaps we need caching, access control, logging, lazy initialization, remote communication, or transaction handling around that service.

One approach is to put all these responsibilities directly inside the real service. Over time, that class becomes responsible for too many concerns.

The Proxy Pattern provides another approach:

  • The client communicates with the proxy.
  • The proxy decides what should happen.
  • The proxy may perform additional work.
  • The proxy delegates to the real object when necessary.

Real-World Analogy

Consider a bank customer trying to access a bank account.

The customer does not directly manipulate the bank's internal database. Instead, the customer interacts through an interface such as a banking application or service layer.

That intermediary can perform operations such as:

  • Checking authentication.
  • Checking authorization.
  • Logging the request.
  • Validating input.
  • Calling the actual account service.

The customer still thinks in terms of the banking operation. The intermediary handles the access-related concerns.

That is the central idea behind a proxy.

Core Structure of the Proxy Pattern

The typical Proxy Pattern contains three important participants:

  • Subject — Defines the common interface.
  • Real Subject — Performs the actual operation.
  • Proxy — Controls access to the real subject and usually implements the same interface.

The relationship can be visualized conceptually as:

Client
   |
   v
Subject
   ^
   |
Proxy ---------> RealSubject

The client depends on the Subject interface rather than directly depending on the concrete real subject.

Basic Java Example

Let us begin with a small example.

interface Image {
    void display();
}

The Image interface represents the common contract.

Now create the real object.

class RealImage implements Image {

    private final String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading " + fileName);
    }

    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }
}

Notice an important detail: loading happens inside the constructor.

That means creating the object can be expensive even if the image is never displayed.

Creating the Proxy

Now we can introduce a proxy that delays creation of the real image until it is actually required.

class ImageProxy implements Image {

    private final String fileName;
    private RealImage realImage;

    public ImageProxy(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }

        realImage.display();
    }
}

The proxy does not immediately create RealImage.

Instead, it creates the real object only when display() is called.

Using the Proxy

public class Main {

    public static void main(String[] args) {

        Image image = new ImageProxy("photo.jpg");

        System.out.println("Image object created.");

        image.display();
        image.display();
    }
}

The output will conceptually look like this:

Image object created.
Loading photo.jpg
Displaying photo.jpg
Displaying photo.jpg

The image was not loaded when the proxy was created. It was loaded only when the image was actually displayed.

Remember: The client works with Image. It does not need to know whether the object is a real image or a proxy.

How the Proxy Works Internally

The basic execution flow is:

  • The client requests an operation through the common interface.
  • The proxy receives the request.
  • The proxy performs any required access or preparation logic.
  • The proxy decides whether the real object must be accessed.
  • The proxy creates or accesses the real object if necessary.
  • The proxy delegates the operation.
  • The result is returned to the client.

The key architectural advantage is that the client does not need to contain the access-management logic.

Lazy Initialization with a Proxy

One of the most common uses of a proxy is lazy initialization.

Lazy initialization means delaying the creation of an expensive object until it is actually needed.

Without a proxy:

ReportService service = new ReportService();
service.generate();

The service is created immediately.

With a proxy:

ReportService service = new ReportServiceProxy();

service.generate();

The proxy can delay the creation of the actual service.

Practical Insight: Lazy proxies are useful when object creation is expensive and the application may not always need the underlying object.

Protection Proxy

A proxy can also control whether a client is allowed to perform an operation.

Suppose an application has a document service.

interface DocumentService {
    void deleteDocument(String documentId);
}

The real service performs the deletion.

class RealDocumentService implements DocumentService {

    @Override
    public void deleteDocument(String documentId) {
        System.out.println("Deleting document: " + documentId);
    }
}

Now create a protection proxy.

class DocumentServiceProxy implements DocumentService {

    private final DocumentService realService;
    private final boolean canDelete;

    public DocumentServiceProxy(
            DocumentService realService,
            boolean canDelete) {

        this.realService = realService;
        this.canDelete = canDelete;
    }

    @Override
    public void deleteDocument(String documentId) {

        if (!canDelete) {
            throw new SecurityException(
                    "User is not allowed to delete documents.");
        }

        realService.deleteDocument(documentId);
    }
}

Now authorization is handled before the real operation takes place.

Using a Protection Proxy

DocumentService realService =
        new RealDocumentService();

DocumentService service =
        new DocumentServiceProxy(realService, false);

service.deleteDocument("DOC-101");

The proxy checks the permission first. Because deletion is not allowed, the real service is never called.

This is commonly called a Protection Proxy.

Common Types of Proxies

The Proxy Pattern is not limited to one specific use case. Different proxies solve different access-related problems.

Proxy Type Purpose Typical Example
Virtual Proxy Delays creation of an expensive object Lazy-loading an image
Protection Proxy Controls access based on permissions Authorization checks
Remote Proxy Represents an object located elsewhere Remote service client
Caching Proxy Stores previously obtained results API response cache
Logging Proxy Records method calls and results Audit logging
Synchronization Proxy Controls concurrent access Thread-sensitive shared resource

Caching Proxy

Suppose a service repeatedly performs an expensive operation.

interface ProductService {
    Product findProduct(String id);
}

A caching proxy can remember previous results.

import java.util.HashMap;
import java.util.Map;

class CachingProductService implements ProductService {

    private final ProductService realService;
    private final Map<String, Product> cache = new HashMap<>();

    public CachingProductService(ProductService realService) {
        this.realService = realService;
    }

    @Override
    public Product findProduct(String id) {

        if (cache.containsKey(id)) {
            return cache.get(id);
        }

        Product product = realService.findProduct(id);
        cache.put(id, product);

        return product;
    }
}

The first request reaches the real service. Later requests can be served from the cache.

In production systems, caching requires additional considerations such as expiration, invalidation, memory limits, concurrency, and consistency.

Logging Proxy

A proxy can also add logging without changing the real service.

interface PaymentService {
    void pay(double amount);
}
class LoggingPaymentProxy implements PaymentService {

    private final PaymentService realService;

    public LoggingPaymentProxy(PaymentService realService) {
        this.realService = realService;
    }

    @Override
    public void pay(double amount) {

        System.out.println("Payment started: " + amount);

        try {
            realService.pay(amount);
            System.out.println("Payment completed.");
        } catch (RuntimeException ex) {
            System.out.println("Payment failed.");
            throw ex;
        }
    }
}

The real payment service remains focused on payment processing while the proxy handles logging concerns.

Remote Proxy

A remote proxy represents an object that exists in another process, server, or network location.

From the client's perspective, the interaction may look like a normal method call:

PaymentService service = new RemotePaymentProxy();

service.pay(5000);

Internally, the proxy may:

  • Serialize the request.
  • Send it across the network.
  • Receive the remote response.
  • Deserialize the result.
  • Translate remote failures into local exceptions.

Modern applications commonly implement similar ideas through HTTP clients, RPC mechanisms, service clients, and generated API clients.

Important: A remote proxy hides communication details from the client, but it does not make network calls behave exactly like local method calls. Latency, timeouts, retries, partial failures, and network errors still exist.

Proxy vs Decorator

Proxy and Decorator can look almost identical structurally because both commonly wrap another object and implement the same interface.

The important difference is their intent.

Aspect Proxy Decorator
Main intent Control access to an object Add responsibilities or behavior
Typical concern Access, security, lazy loading, remote access, caching Additional features or behavior
Client awareness Often unaware that a proxy exists Usually composition is intentional
Underlying object Often represents or manages access to a specific real object Usually wraps another component to extend behavior

The distinction is primarily about intent rather than syntax.

Proxy vs Adapter

Both patterns can sit between a client and another object, but they solve different problems.

Aspect Proxy Adapter
Primary goal Control or manage access Make incompatible interfaces work together
Interface Usually exposes the same interface Provides a different interface expected by the client
Example Authorization proxy Legacy API adapter

Proxy vs Facade

A Facade provides a simplified interface to a subsystem. A Proxy generally preserves the interface of the object it represents while controlling access to it.

For example, a facade might expose:

orderService.placeOrder(order);

Internally, it may coordinate inventory, payment, shipping, and notification systems.

A proxy, on the other hand, might expose the same service contract while adding authorization or caching before forwarding the call.

Proxy and Composition

The Proxy Pattern relies heavily on composition.

The proxy contains a reference to another object:

class ServiceProxy implements Service {

    private final Service realService;

    // ...
}

This is often preferable to inheritance because the proxy can wrap an existing implementation without modifying its class hierarchy.

It also makes the proxy easy to replace:

Service service =
        new ServiceProxy(new RealService());

Proxy Chains

Multiple proxies can be combined.

Service service =
    new LoggingProxy(
        new CachingProxy(
            new AuthorizationProxy(
                new RealService())));

A request may therefore pass through several layers before reaching the real service.

Client
  |
  v
Logging Proxy
  |
  v
Caching Proxy
  |
  v
Authorization Proxy
  |
  v
Real Service

This can be powerful, but excessive proxy layers can make execution flow difficult to understand.

Proxy and Java Dynamic Proxies

Java also provides a mechanism for creating proxies dynamically at runtime through java.lang.reflect.Proxy.

Instead of manually creating a separate proxy class for every interface, Java can create a proxy object dynamically.

import java.lang.reflect.Proxy;

PaymentService proxy =
        (PaymentService) Proxy.newProxyInstance(
            PaymentService.class.getClassLoader(),
            new Class<?>[] { PaymentService.class },
            (object, method, args) -> {

                System.out.println(
                        "Calling: " + method.getName());

                return method.invoke(realService, args);
            });

The exact implementation depends on the application and exception-handling requirements, but the idea is important: Java can generate proxy implementations dynamically at runtime.

Proxy Pattern and Frameworks

The proxy idea appears frequently in enterprise Java and framework-based applications.

For example, framework infrastructure may use proxy objects to provide behavior such as:

  • Transaction management.
  • Security checks.
  • Method interception.
  • Lazy loading.
  • Caching.
  • Logging.

This is one reason understanding the Proxy Pattern is valuable beyond design-pattern interview questions. Once you recognize the pattern, many framework behaviors become easier to understand.

Advantages of the Proxy Pattern

  • Access control: The proxy can restrict operations.
  • Lazy loading: Expensive objects can be created only when needed.
  • Caching: Frequently requested results can be reused.
  • Logging: Calls can be monitored without changing the real object.
  • Remote access: Network communication can be hidden behind a familiar interface.
  • Separation of concerns: Access-related behavior can remain outside the real service.
  • Transparent substitution: The client can generally work with the same interface.

Disadvantages of the Proxy Pattern

  • Adds another layer of indirection.
  • Can make debugging more complicated.
  • Too many proxies can make control flow difficult to follow.
  • Caching introduces invalidation and consistency concerns.
  • Remote proxies cannot eliminate network failures or latency.
  • Security logic can become dangerous if authorization is incomplete or incorrectly implemented.

Common Beginner Mistakes

1. Confusing Proxy with Adapter

An adapter primarily changes an interface so incompatible components can work together. A proxy generally preserves the interface while controlling access to another object.

2. Putting Business Logic Inside the Proxy

A proxy may perform access-related logic, caching, logging, or similar cross-cutting behavior. It should not gradually become the application's main business-service implementation.

3. Assuming Every Wrapper Is a Proxy

A wrapper can represent several patterns. Intent matters. A decorator adds responsibilities, an adapter changes interfaces, and a proxy controls access.

4. Ignoring Thread Safety

A caching or lazy-loading proxy may maintain mutable state. If multiple threads can access it, the implementation must be designed appropriately for concurrency.

5. Forgetting Failure Handling

Remote proxies, caching proxies, and service proxies can encounter failures. A proxy should not silently hide important errors merely to keep the client code simple.

Best Practices

  • Keep the proxy contract aligned with the subject interface.
  • Give the proxy a focused responsibility.
  • Prefer composition over inheritance for wrapping the real object.
  • Keep business rules in the appropriate business layer.
  • Design caching behavior explicitly, including invalidation when required.
  • Consider thread safety for shared proxy state.
  • Make remote failure and timeout behavior explicit.
  • Avoid unnecessary proxy layers.
  • Test both proxy behavior and delegation behavior.

When Should You Use the Proxy Pattern?

The Proxy Pattern is a strong candidate when you need an intermediary that controls access to an existing object without forcing the client to know the access details.

Typical situations include:

  • Expensive objects should be loaded lazily.
  • Access must be authorized.
  • Results should be cached.
  • Operations need logging or monitoring.
  • The real object is remote.
  • Concurrent access requires coordination.
  • The existing service should remain unchanged while access behavior is added externally.

When Should You Avoid It?

Do not introduce a proxy simply because wrapping an object is possible.

If the intermediary adds no meaningful access-control, lifecycle, performance, communication, or cross-cutting responsibility, the additional abstraction may only make the code harder to understand.

Practical Rule: Introduce a proxy when controlling access provides a clear architectural benefit. Do not create wrapper classes without a reason.

Proxy Pattern and SOLID Principles

The Proxy Pattern can support several SOLID principles when used carefully.

  • Single Responsibility Principle: Access-related concerns can be separated from the real service.
  • Open/Closed Principle: New access behavior can sometimes be introduced without modifying the real implementation.
  • Dependency Inversion Principle: Both client and proxy can depend on an abstraction rather than a concrete implementation.

The pattern itself does not automatically guarantee SOLID design. The quality of the implementation still matters.

Testing a Proxy

A good proxy test should verify both its own behavior and its interaction with the real service.

For example, a protection proxy should be tested for:

  • Allowed access.
  • Denied access.
  • Correct delegation.
  • Correct handling of failures.

A caching proxy should additionally be tested to confirm that repeated requests do not unnecessarily call the underlying service.

This is particularly useful when using mocks or test doubles to verify invocation counts.

Interview Insight: What Is the Proxy Pattern?

A strong interview answer is:

The Proxy Pattern provides a substitute object that controls access to another object while usually exposing the same interface. It can be used for lazy loading, security, caching, remote access, logging, and other access-related concerns.

Interview Insight: Proxy vs Decorator

A common interview follow-up is: “If both Proxy and Decorator wrap objects, how are they different?”

The answer is primarily intent:

  • Proxy: controls access to an object.
  • Decorator: adds responsibilities or behavior to an object.

Their structures can look similar, which is why identifying the intent is more reliable than identifying the pattern from syntax alone.

Interview Insight: What Is a Virtual Proxy?

A virtual proxy delays creation of an expensive object until the object is actually needed.

A classic example is an image proxy that postpones loading a large image from disk until display() is called.

Interview Insight: What Is a Protection Proxy?

A protection proxy controls access to an operation based on permissions or security rules.

For example, a proxy can check whether a user has permission to delete a document before delegating the request to the real document service.

Quick Learning Checkpoint

Before moving forward, make sure you can answer these questions:

  • What problem does the Proxy Pattern solve?
  • Why does a proxy commonly implement the same interface as the real subject?
  • How does a virtual proxy implement lazy loading?
  • What is the purpose of a protection proxy?
  • How can a proxy implement caching?
  • How is a proxy different from a decorator?
  • How is a proxy different from an adapter?
  • Why can excessive proxy layers become difficult to maintain?

Quick Revision

Concept Key Idea
Proxy Pattern Controls access to another object through a substitute.
Subject Common interface used by the client and proxy.
Real Subject Performs the actual operation.
Proxy Intercepts and manages access to the real subject.
Virtual Proxy Delays expensive object creation.
Protection Proxy Controls access using permissions.
Caching Proxy Stores and reuses results.
Remote Proxy Represents an object located remotely.
Logging Proxy Adds monitoring or logging around calls.
Main distinction Proxy focuses on access control; Decorator focuses on adding behavior.

Final Takeaway

The Proxy Pattern introduces a controlled gateway between a client and a real object. Instead of allowing every client to directly access the underlying object, the proxy can decide when, how, and whether that access should happen.

That simple idea supports surprisingly powerful techniques: lazy loading, authorization, caching, logging, remote communication, synchronization, and other access-management mechanisms.

The most important lesson is to remember the pattern's intent: a proxy controls access to an object while allowing the client to work through a familiar abstraction.

Post a Comment

0Comments
Post a Comment (0)