Proxy Design Pattern
Provide a surrogate or placeholder for another object to control access to it.
Real-World Analogy
Think of a Credit Card
A credit card is a proxy for your bank account. It provides the same interface (you can pay with it) but adds access control (credit limit), lazy loading (doesn't transfer money immediately), and logging (transaction history). The bank account is the real subject; the credit card is its proxy.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
You["🛒 You"] -->|"pays"| CC["💳 Credit Card"]
CC -->|"delegates"| Bank["🏦 Bank Account"]
Bank -->|"confirms"| Done["✅ Done!"]
style You fill:#E3F2FD,stroke:#1565C0,color:#000
style CC fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
style Bank fill:#E8F5E9,stroke:#2E7D32,color:#000
style Done fill:#FFF8E1,stroke:#F9A825,color:#000
Pattern Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Client["🖥️ Client"] -->|"uses"| Subject[["🎯 Subject"]]
Proxy{{"🛡️ Proxy"}} -->|"implements"| Subject
Proxy -.->|"delegates"| Real(["📦 Real Subject"])
Real -->|"implements"| Subject
style Client fill:#E8F5E9,stroke:#2E7D32,color:#000
style Subject fill:#FFF3E0,stroke:#E65100,color:#000
style Proxy fill:#E3F2FD,stroke:#1565C0,color:#000
style Real fill:#E3F2FD,stroke:#1565C0,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class Image {
<<interface>>
+display() void
+getFilename() String
}
class HighResolutionImage {
-filename: String
-imageData: byte[]
+display() void
+getFilename() String
}
class ImageProxy {
-filename: String
-realImage: HighResolutionImage
+display() void
+getFilename() String
}
HighResolutionImage ..|> Image : implements
ImageProxy ..|> Image : implements
ImageProxy --> HighResolutionImage : lazy delegates to Types of Proxies
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Proxy{{"🛡️ Proxy Pattern"}} --> Virtual(["💤 Virtual"])
Virtual --> Protection(["🔒 Protection"])
Protection --> Remote(["🌐 Remote"])
Remote --> Cache(["⚡ Caching"])
Cache --> Logging(["📝 Logging"])
style Proxy fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
style Virtual fill:#E3F2FD,stroke:#1565C0,color:#000
style Protection fill:#E3F2FD,stroke:#1565C0,color:#000
style Remote fill:#E3F2FD,stroke:#1565C0,color:#000
style Cache fill:#E3F2FD,stroke:#1565C0,color:#000
style Logging fill:#E3F2FD,stroke:#1565C0,color:#000
The Problem
Consider a massive Document object that loads a high-resolution image from disk. Loading this image takes 5 seconds. But the user might never scroll down to see it. You don't want to:
- Load expensive resources that might never be used (wasted RAM/time)
- Give all users access to sensitive operations
- Make direct network calls without caching or retrying
Without This Pattern
// BAD: All images loaded eagerly — even if user never scrolls to them
public class DocumentViewer {
private List<HighResolutionImage> images;
public DocumentViewer(List<String> imageFiles) {
this.images = new ArrayList<>();
for (String file : imageFiles) {
// Each image takes 5 seconds to load and 10MB of RAM
images.add(new HighResolutionImage(file)); // ALL loaded upfront!
}
// 50 images = 250 seconds startup, 500MB RAM
// User may only view 2 of them
}
// No access control — anyone can delete
public void deleteImage(int index) {
images.remove(index); // No permission check!
}
// No caching — repeated calls hit disk every time
public byte[] getImageData(String filename) {
return loadFromDisk(filename); // 5 seconds EVERY call
}
}
Problems:
- Wasted resources on eager loading: All 50 high-resolution images are loaded at startup (250 seconds, 500MB RAM) even though the user may only view 2 — no lazy initialization
- No access control: Sensitive operations like
deleteare exposed to all callers with no permission checks — any code path can destroy data - No caching: Repeated requests for the same image re-read from disk every time, wasting I/O and making the UI unresponsive
- Violates Single Responsibility: The
DocumentVieweris forced to handle loading optimization, access control, and caching itself — or simply not handle them at all - Pain point: Users complain that the application takes 4 minutes to open a document, uses 500MB of RAM for images they never look at, and an intern accidentally deletes production data because there are no access checks
The Solution
The Proxy object sits between the client and the real object. It implements the same interface, so the client doesn't know it's talking to a proxy. Depending on the type:
| Proxy Type | Purpose |
|---|---|
| Virtual | Defers creation of expensive objects until actually needed |
| Protection | Controls access based on permissions/roles |
| Remote | Represents an object in a different address space |
| Caching | Stores results to avoid repeated expensive operations |
| Logging | Records requests for diagnostics |
Implementation
// Subject interface
public interface Image {
void display();
String getFilename();
}
// Real Subject — expensive to create
public class HighResolutionImage implements Image {
private final String filename;
private final byte[] imageData;
public HighResolutionImage(String filename) {
this.filename = filename;
this.imageData = loadFromDisk(filename); // EXPENSIVE!
System.out.println("Loaded image: " + filename + " (" + imageData.length + " bytes)");
}
@Override
public void display() {
System.out.println("Displaying: " + filename);
}
@Override
public String getFilename() {
return filename;
}
private byte[] loadFromDisk(String filename) {
// Simulate expensive I/O operation
try { Thread.sleep(3000); } catch (InterruptedException e) {}
return new byte[10_000_000]; // 10MB image
}
}
// Virtual Proxy — defers loading until display() is called
public class ImageProxy implements Image {
private final String filename;
private HighResolutionImage realImage; // lazy-loaded
public ImageProxy(String filename) {
this.filename = filename;
// NO loading here — that's the point!
}
@Override
public void display() {
if (realImage == null) {
realImage = new HighResolutionImage(filename);
}
realImage.display();
}
@Override
public String getFilename() {
return filename; // No need to load image for this
}
}
// Client
public class DocumentViewer {
public static void main(String[] args) {
// Images are NOT loaded yet — just proxies
List<Image> images = List.of(
new ImageProxy("photo1.png"),
new ImageProxy("photo2.png"),
new ImageProxy("photo3.png")
);
// Only loads when user actually views
System.out.println("Document opened. Scrolling to image 2...");
images.get(1).display(); // Only THIS image gets loaded
}
}
// Subject interface
public interface Document {
void read();
void write(String content);
void delete();
}
// Real Subject
public class SensitiveDocument implements Document {
private String content;
private final String name;
public SensitiveDocument(String name, String content) {
this.name = name;
this.content = content;
}
@Override
public void read() {
System.out.println("Reading document '" + name + "': " + content);
}
@Override
public void write(String content) {
this.content = content;
System.out.println("Document '" + name + "' updated.");
}
@Override
public void delete() {
System.out.println("Document '" + name + "' deleted.");
}
}
// Protection Proxy — enforces role-based access
public class DocumentProxy implements Document {
private final SensitiveDocument realDocument;
private final User currentUser;
public DocumentProxy(SensitiveDocument doc, User currentUser) {
this.realDocument = doc;
this.currentUser = currentUser;
}
@Override
public void read() {
if (currentUser.hasPermission(Permission.READ)) {
realDocument.read();
} else {
throw new SecurityException("Access denied: READ permission required");
}
}
@Override
public void write(String content) {
if (currentUser.hasPermission(Permission.WRITE)) {
realDocument.write(content);
} else {
throw new SecurityException("Access denied: WRITE permission required");
}
}
@Override
public void delete() {
if (currentUser.hasRole(Role.ADMIN)) {
realDocument.delete();
} else {
throw new SecurityException("Access denied: ADMIN role required");
}
}
}
// Supporting classes
public enum Permission { READ, WRITE, DELETE }
public enum Role { VIEWER, EDITOR, ADMIN }
public class User {
private final String name;
private final Role role;
private final Set<Permission> permissions;
public User(String name, Role role, Set<Permission> permissions) {
this.name = name;
this.role = role;
this.permissions = permissions;
}
public boolean hasPermission(Permission p) { return permissions.contains(p); }
public boolean hasRole(Role r) { return this.role == r; }
}
// Subject interface
public interface WeatherService {
WeatherData getWeather(String city);
}
// Real Subject — makes expensive API call
public class RealWeatherService implements WeatherService {
@Override
public WeatherData getWeather(String city) {
System.out.println("Calling external weather API for: " + city);
// Simulate HTTP call to weather API
return new WeatherData(city, 72.0, "Sunny");
}
}
// Caching Proxy — avoids repeated API calls
public class CachingWeatherProxy implements WeatherService {
private final RealWeatherService realService;
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
private final Duration cacheTtl;
public CachingWeatherProxy(Duration cacheTtl) {
this.realService = new RealWeatherService();
this.cacheTtl = cacheTtl;
}
@Override
public WeatherData getWeather(String city) {
CacheEntry entry = cache.get(city);
if (entry != null && !entry.isExpired(cacheTtl)) {
System.out.println("Cache HIT for: " + city);
return entry.data();
}
System.out.println("Cache MISS for: " + city);
WeatherData data = realService.getWeather(city);
cache.put(city, new CacheEntry(data, Instant.now()));
return data;
}
private record CacheEntry(WeatherData data, Instant timestamp) {
boolean isExpired(Duration ttl) {
return Instant.now().isAfter(timestamp.plus(ttl));
}
}
}
// Usage
public class App {
public static void main(String[] args) {
WeatherService service = new CachingWeatherProxy(Duration.ofMinutes(5));
service.getWeather("NYC"); // Cache MISS — calls API
service.getWeather("NYC"); // Cache HIT — returns cached
service.getWeather("LA"); // Cache MISS — calls API
}
}
// Java's built-in dynamic proxy mechanism
public class LoggingProxyFactory {
@SuppressWarnings("unchecked")
public static <T> T createLoggingProxy(T target, Class<T> interfaceType) {
return (T) Proxy.newProxyInstance(
interfaceType.getClassLoader(),
new Class<?>[]{ interfaceType },
new LoggingHandler(target)
);
}
private static class LoggingHandler implements InvocationHandler {
private final Object target;
private static final Logger log = LoggerFactory.getLogger(LoggingHandler.class);
LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
log.info("Calling: {}.{}({})", target.getClass().getSimpleName(),
method.getName(), Arrays.toString(args));
try {
Object result = method.invoke(target, args);
long elapsed = (System.nanoTime() - start) / 1_000_000;
log.info("Completed: {}.{} in {}ms",
target.getClass().getSimpleName(), method.getName(), elapsed);
return result;
} catch (InvocationTargetException e) {
log.error("Exception in {}.{}: {}",
target.getClass().getSimpleName(), method.getName(),
e.getTargetException().getMessage());
throw e.getTargetException();
}
}
}
}
// Usage
UserService realService = new UserServiceImpl();
UserService proxiedService = LoggingProxyFactory.createLoggingProxy(realService, UserService.class);
proxiedService.findById(1L); // Automatically logged!
When to Use
- Lazy initialization (Virtual Proxy) — defer costly object creation
- Access control (Protection Proxy) — restrict who can do what
- Remote objects (Remote Proxy) — represent objects on remote servers (RMI, gRPC)
- Caching (Caching Proxy) — store results of expensive operations
- Logging/Auditing — transparently record all interactions
- Smart references — perform actions when an object is accessed (reference counting, etc.)
Real-World Examples
| Where | Example |
|---|---|
| Spring | AOP Proxies — @Transactional, @Cacheable, @Async all use CGLIB/JDK proxies |
| Spring | @Lazy annotation creates a virtual proxy for bean injection |
| Hibernate | Lazy loading of entities — user.getOrders() returns a proxy |
| JDK | java.lang.reflect.Proxy — dynamic proxy mechanism |
| JDK | java.rmi.* — remote method invocation uses proxy stubs |
| MyBatis | Mapper interfaces are proxies that generate SQL |
| Feign | HTTP client interfaces are proxies for REST calls |
Pitfalls
Common Mistakes
- Performance overhead: Each method call goes through the proxy — avoid for hot paths unless necessary
- Hibernate N+1 problem: Lazy proxies that trigger individual queries in a loop
- Debugging difficulty: Stack traces through proxies are harder to read (especially CGLIB)
- Proxy identity:
proxy != realObject— be careful with equals/hashCode - Confusing Proxy with Decorator: Proxy controls access; Decorator adds behavior. Proxy usually creates the real object; Decorator receives it
Key Takeaways
Summary
| Aspect | Detail |
|---|---|
| Intent | Control access to an object through a surrogate |
| Mechanism | Same interface as subject; intercepts and delegates calls |
| Key Benefit | Separates concerns (access control, caching, lazy-init) from business logic |
| Key Variants | Virtual, Protection, Remote, Caching, Logging |
| Spring Connection | AOP is fundamentally implemented using proxies |
| Interview Tip | "Every @Transactional method in Spring works because of a Proxy wrapping your bean" |