
Proxy Design Pattern Explained with Examples
The Proxy Design Pattern is a structural design pattern that acts as an intermediary between a client and a real object, controlling access to it while potentially adding extra functionality like caching, lazy loading, or security checks. This pattern is crucial for developers building scalable applications because it helps manage resource-intensive operations, implements access control mechanisms, and provides a clean way to add cross-cutting concerns without modifying existing code. In this post, you’ll learn how to implement various types of proxies, understand their real-world applications, and discover best practices for avoiding common implementation pitfalls.
How the Proxy Pattern Works
The proxy pattern involves three main components: the Subject interface that defines common operations, the RealSubject that performs actual work, and the Proxy that controls access to the RealSubject. The proxy maintains a reference to the real object and delegates requests to it when appropriate.
Here’s the basic structure in Java:
// Subject interface
interface DatabaseService {
String fetchData(String query);
}
// Real implementation
class RealDatabaseService implements DatabaseService {
@Override
public String fetchData(String query) {
// Simulate expensive database operation
System.out.println("Executing query: " + query);
try {
Thread.sleep(2000); // Simulate network delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Data for query: " + query;
}
}
// Proxy implementation
class CachingDatabaseProxy implements DatabaseService {
private RealDatabaseService realService;
private Map<String, String> cache = new HashMap<>();
@Override
public String fetchData(String query) {
if (cache.containsKey(query)) {
System.out.println("Cache hit for: " + query);
return cache.get(query);
}
if (realService == null) {
realService = new RealDatabaseService();
}
String result = realService.fetchData(query);
cache.put(query, result);
return result;
}
}
Types of Proxy Patterns
There are several types of proxy patterns, each serving different purposes:
Proxy Type | Purpose | Use Case | Performance Impact |
---|---|---|---|
Virtual Proxy | Lazy initialization | Loading large images or files | Reduces initial load time |
Protection Proxy | Access control | User authentication and authorization | Minimal overhead |
Remote Proxy | Network communication | Web services, RMI | Network latency dependent |
Caching Proxy | Performance optimization | Database queries, API calls | Significant improvement with cache hits |
Step-by-Step Implementation Guide
Let’s implement a comprehensive proxy example that combines multiple proxy features. We’ll create a file system proxy that handles caching, access control, and lazy loading:
// Step 1: Define the subject interface
interface FileSystem {
String readFile(String filename, String user);
void writeFile(String filename, String content, String user);
}
// Step 2: Implement the real subject
class RealFileSystem implements FileSystem {
@Override
public String readFile(String filename, String user) {
System.out.println("Reading file from disk: " + filename);
// Simulate file reading
return "Content of " + filename;
}
@Override
public void writeFile(String filename, String content, String user) {
System.out.println("Writing to disk: " + filename);
// Simulate file writing
}
}
// Step 3: Create the proxy with multiple features
class FileSystemProxy implements FileSystem {
private RealFileSystem realFileSystem;
private Map<String, String> fileCache = new HashMap<>();
private Set<String> authorizedUsers = Set.of("admin", "user1", "user2");
private Map<String, Long> accessLog = new HashMap<>();
@Override
public String readFile(String filename, String user) {
// Protection proxy functionality
if (!isAuthorized(user)) {
throw new SecurityException("Access denied for user: " + user);
}
// Logging functionality
logAccess(user, "READ", filename);
// Caching proxy functionality
String cacheKey = filename + "_" + user;
if (fileCache.containsKey(cacheKey)) {
System.out.println("Cache hit for: " + filename);
return fileCache.get(cacheKey);
}
// Virtual proxy functionality - lazy initialization
if (realFileSystem == null) {
System.out.println("Initializing real file system...");
realFileSystem = new RealFileSystem();
}
String content = realFileSystem.readFile(filename, user);
fileCache.put(cacheKey, content);
return content;
}
@Override
public void writeFile(String filename, String content, String user) {
if (!isAuthorized(user)) {
throw new SecurityException("Access denied for user: " + user);
}
logAccess(user, "WRITE", filename);
if (realFileSystem == null) {
realFileSystem = new RealFileSystem();
}
realFileSystem.writeFile(filename, content, user);
// Invalidate cache
String cacheKey = filename + "_" + user;
fileCache.remove(cacheKey);
}
private boolean isAuthorized(String user) {
return authorizedUsers.contains(user);
}
private void logAccess(String user, String operation, String filename) {
String logKey = user + "_" + operation + "_" + filename;
accessLog.put(logKey, System.currentTimeMillis());
System.out.println("Logged: " + user + " performed " + operation + " on " + filename);
}
}
Real-World Examples and Use Cases
The proxy pattern appears frequently in enterprise applications and system architecture. Here are some practical examples:
- Web Servers and Reverse Proxies: Nginx and Apache act as proxies, handling SSL termination, load balancing, and caching before forwarding requests to application servers
- ORM Frameworks: Hibernate uses proxy objects for lazy loading of entity relationships, creating proxy instances that fetch data only when accessed
- Security Gateways: API gateways like Kong or AWS API Gateway proxy requests while adding authentication, rate limiting, and monitoring
- CDN Services: Content delivery networks act as caching proxies, serving static content from edge locations closest to users
Here’s a practical example of implementing a REST API proxy with rate limiting:
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
class RateLimitingAPIProxy {
private final APIService realService;
private final Map<String, RateLimitInfo> rateLimits = new HashMap<>();
private final int maxRequestsPerMinute = 60;
public RateLimitingAPIProxy(APIService realService) {
this.realService = realService;
}
public APIResponse makeRequest(String clientId, APIRequest request) {
if (!isWithinRateLimit(clientId)) {
return new APIResponse(429, "Rate limit exceeded");
}
updateRateLimit(clientId);
return realService.makeRequest(request);
}
private boolean isWithinRateLimit(String clientId) {
RateLimitInfo info = rateLimits.get(clientId);
if (info == null) {
return true;
}
LocalDateTime now = LocalDateTime.now();
long minutesSinceReset = ChronoUnit.MINUTES.between(info.windowStart, now);
if (minutesSinceReset >= 1) {
// Reset the window
info.requestCount = 0;
info.windowStart = now;
return true;
}
return info.requestCount < maxRequestsPerMinute;
}
private void updateRateLimit(String clientId) {
rateLimits.computeIfAbsent(clientId, k -> new RateLimitInfo())
.requestCount++;
}
private static class RateLimitInfo {
int requestCount = 0;
LocalDateTime windowStart = LocalDateTime.now();
}
}
Performance Considerations and Benchmarks
The proxy pattern can significantly impact performance depending on implementation. Here’s a comparison of different proxy implementations:
Scenario | Direct Access | Simple Proxy | Caching Proxy (Cold) | Caching Proxy (Warm) |
---|---|---|---|---|
Database Query (ms) | 1200 | 1205 | 1210 | 2 |
File Read (ms) | 150 | 152 | 155 | 1 |
API Call (ms) | 800 | 805 | 810 | 5 |
Memory Overhead | 0MB | 0.1MB | 2-50MB | 2-50MB |
When running on VPS environments, caching proxies can reduce database load by up to 80% in typical web applications. For high-traffic applications requiring consistent performance, consider deploying on dedicated servers to ensure adequate memory for cache storage.
Comparisons with Alternative Patterns
The proxy pattern is often confused with similar patterns. Here’s how it differs:
Pattern | Intent | When to Use | Key Difference |
---|---|---|---|
Proxy | Control access to another object | Need lazy loading, caching, or access control | Same interface as real object |
Decorator | Add behavior to objects dynamically | Need to extend functionality without inheritance | Focuses on adding new behavior |
Adapter | Make incompatible interfaces work together | Integrating legacy or third-party code | Converts one interface to another |
Facade | Provide simplified interface to complex subsystem | Need to hide complexity from clients | Simplifies rather than controls access |
Best Practices and Common Pitfalls
Implementing proxies effectively requires attention to several key areas:
- Memory Management: Caching proxies can consume significant memory. Implement cache eviction policies like LRU or time-based expiration
- Thread Safety: Ensure proxy implementations are thread-safe when used in concurrent environments
- Error Handling: Properly handle exceptions from the real object and decide whether to cache error responses
- Monitoring: Add metrics and logging to track proxy performance and cache hit rates
Here’s an improved caching proxy with proper resource management:
class ImprovedCachingProxy implements AutoCloseable {
private final ExecutorService executor = Executors.newCachedThreadPool();
private final Cache<String, String> cache;
private final RealService realService;
public ImprovedCachingProxy() {
this.cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
this.realService = new RealService();
}
public CompletableFuture<String> getData(String key) {
String cachedValue = cache.getIfPresent(key);
if (cachedValue != null) {
return CompletableFuture.completedFuture(cachedValue);
}
return CompletableFuture.supplyAsync(() -> {
String value = realService.getData(key);
cache.put(key, value);
return value;
}, executor);
}
public CacheStats getCacheStats() {
return cache.stats();
}
@Override
public void close() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Common pitfalls to avoid include:
- Over-caching: Don’t cache everything; focus on expensive operations with high reuse potential
- Ignoring Cache Invalidation: Implement proper cache invalidation strategies to avoid serving stale data
- Poor Error Propagation: Ensure errors from the real object are properly handled and propagated to clients
- Resource Leaks: Always clean up resources in long-running proxy instances
For more advanced proxy implementations, consider exploring the Java Dynamic Proxy API or the CGLIB library for runtime proxy generation. The Spring Framework also provides excellent AOP proxy capabilities that implement many of these patterns automatically.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.