June 17, 2026 in patterns5 minutes
A intermediate-level guide to Proxy: before-and-after java code and diagrams for a CS student.
Imagine you are at a bank. You want to withdraw money from the vault, but you do not walk into the vault yourself. Instead, you talk to a teller. The teller performs “housekeeping” tasks: they check your ID, verify your signature, and ensure you actually have enough funds. Only after these checks are passed does the teller facilitate access to the actual money.
In software, we often face a similar problem where a single class tries to be both the “vault” (doing the core work) and the “teller” (managing security, logging, or resource loading). This leads to a violation of the Single Responsibility Principle (SRP), which states that a class should have only one reason to change. When your business logic is tangled with permission checks, the class becomes harder to test and maintain.
public class DocumentService {
// The service is cluttered with housekeeping/security logic
public void openDocument(String userId, String documentId) {
// Security check (Housekeeping)
if ("ADMIN".equals(userId)) {
System.out.println("Access granted to all documents.");
} else if (!checkPermissions(userId, documentId)) {
throw new SecurityException("Unauthorized access");
}
// Core business logic (Real Subject work)
loadHeavyDocumentFromDisk(documentId);
}
private boolean checkPermissions(String userId, String docId) {
return true; // Mock permission check
}
private void loadHeavyDocumentFromDisk(String docId) {
System.out.println("Loading document " + docId + " from disk (heavy operation)...");
}
}In this example, DocumentService is doing two unrelated jobs: enforcing security rules via checkPermissions and performing the heavy work of loading files via loadHeavyDocumentFromDisk. The core logic is trapped inside a web of conditional checks.
classDiagram
class DocumentService {
+openDocument(String userId, String documentId) void
-checkPermissions(String userId, String docId) boolean
-loadHeavyDocumentFromDisk(String docId) void
}
The Proxy pattern solves this by introducing an intermediary object. A Proxy is a structural design pattern that provides a surrogate or placeholder for another object to control access to it.
To make this work seamlessly, the Proxy must implement the exact same interface as the real object. This allows the client (the code using the service) to interact with either the real service or the proxy without knowing anything has changed. We call the actual worker the “Real Subject” and the intermediary the “Proxy.”
interface DocumentService {
void openDocument(String userId, String documentId);
}
class RealDocumentService implements DocumentService {
@Override
public void openDocument(String userId, String documentId) {
loadHeavyDocumentFromDisk(documentId);
}
private void loadHeavyDocumentFromDisk(String docId) {
System.out.println("Loading document " + docId + " from disk (heavy operation)...");
}
}
class DocumentSecurityProxy implements DocumentService {
private final DocumentService realService;
public DocumentSecurityProxy(DocumentService realService) {
this.realService = realService;
}
@Override
public void openDocument(String userId, String documentId) {
// The proxy handles the housekeeping/security concerns
if ("ADMIN".equals(userId)) {
System.out.println("Access granted to all documents.");
realService.openDocument(userId, documentId);
} else if (!checkPermissions(userId, documentId)) {
throw new SecurityException("Unauthorized access");
} else {
realService.openDocument(userId, documentId);
}
}
private boolean checkPermissions(String userId, String docId) {
return true; // Mock permission check
}
}By extracting the security logic into DocumentSecurityProxy, we have achieved two things:
RealDocumentService now only cares about loading documents.DocumentSecurityProxy handles the “housekeeping” of verifying users and managing access.The following sequence shows how the client’s call to openDocument is intercepted by the proxy, which then decides whether to delegate the work to the real service or throw an exception.
sequenceDiagram
participant Client
participant Proxy as DocumentSecurityProxy
participant Real as RealDocumentService
Client ->> Proxy: openDocument(userId, documentId)
alt is user ADMIN?
Proxy ->> Real: openDocument(userId, documentId)
Real -->> Proxy: void
Proxy -->> Client: void
else checkPermissions fails
Proxy -->> Client: throw SecurityException
end
While our example focuses on a Protection Proxy (which controls access based on permissions), proxies are used in several ways:
It is common to confuse the Proxy with other structural patterns like Decorator or Adapter. Here is how to tell them apart:
classDiagram
class DocumentService {
<<interface>>
+openDocument(String userId, String documentId) void
}
class RealDocumentService {
+openDocument(String userId, String documentId) void
}
class DocumentSecurityProxy {
-DocumentService realService
+openDocument(String userId, String documentId) void
}
RealDocumentService ..|> DocumentService : implements
DocumentSecurityProxy ..|> DocumentService : implements
DocumentSecurityProxy --> RealDocumentService : associates
The Proxy pattern is highly effective when you need to manage complex lifecycles or enforce cross-cutting concerns like security, logging, or caching without polluting your business logic. It keeps your “Real Subject” clean and focused on its primary task.
However, avoid using a Proxy if:
Using the Proxy instead of the real service to enforce security automatically
DocumentService service = new DocumentSecurityProxy(new RealDocumentService());
service.openDocument("USER_123", "DOC_001");