BLOG POSTS
Proxy Design Pattern Explained with Examples

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.

Leave a reply

Your email address will not be published. Required fields are marked