Proxy Pattern for Controlled Access in Java

Introduction

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. This structural pattern is used for lazy initialization, access control, logging, monitoring, and other cross-cutting concerns without modifying the original object's code.

Core Proxy Types

1. Virtual Proxy (Lazy Initialization)

/**
* Interface for image operations
*/
public interface Image {
void display();
String getFileName();
long getFileSize();
}
/**
* Real subject - expensive to create
*/
public class HighResolutionImage implements Image {
private final String fileName;
private final long fileSize;
public HighResolutionImage(String fileName) {
this.fileName = fileName;
loadImageFromDisk(); // Expensive operation
this.fileSize = calculateFileSize();
}
private void loadImageFromDisk() {
System.out.println("Loading high-resolution image: " + fileName);
// Simulate expensive loading operation
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Image loaded: " + fileName);
}
private long calculateFileSize() {
// Simulate file size calculation
return fileName.length() * 1024L;
}
@Override
public void display() {
System.out.println("Displaying high-resolution image: " + fileName);
}
@Override
public String getFileName() {
return fileName;
}
@Override
public long getFileSize() {
return fileSize;
}
}
/**
* Virtual Proxy - delays expensive object creation until needed
*/
public class ImageProxy implements Image {
private final String fileName;
private HighResolutionImage realImage;
public ImageProxy(String fileName) {
this.fileName = fileName;
System.out.println("Image proxy created for: " + fileName);
}
private void initializeRealImage() {
if (realImage == null) {
realImage = new HighResolutionImage(fileName);
}
}
@Override
public void display() {
initializeRealImage();
realImage.display();
}
@Override
public String getFileName() {
return fileName;
}
@Override
public long getFileSize() {
initializeRealImage();
return realImage.getFileSize();
}
// Additional proxy methods
public boolean isImageLoaded() {
return realImage != null;
}
public void preload() {
initializeRealImage();
}
}

2. Protection Proxy (Access Control)

/**
* Database operation interface
*/
public interface Database {
void query(String sql);
void update(String sql);
void delete(String sql);
void createTable(String tableName);
}
/**
* Real database implementation
*/
public class RealDatabase implements Database {
private final String databaseName;
public RealDatabase(String databaseName) {
this.databaseName = databaseName;
System.out.println("Connected to database: " + databaseName);
}
@Override
public void query(String sql) {
System.out.println("Executing query: " + sql);
// Actual database query execution
}
@Override
public void update(String sql) {
System.out.println("Executing update: " + sql);
// Actual database update
}
@Override
public void delete(String sql) {
System.out.println("Executing delete: " + sql);
// Actual database delete
}
@Override
public void createTable(String tableName) {
System.out.println("Creating table: " + tableName);
// Actual table creation
}
}
/**
* User roles for access control
*/
public enum UserRole {
GUEST,      // Can only query
USER,       // Can query and update
ADMIN,      // Can do everything
DEVELOPER   // Can create tables
}
/**
* User entity
*/
public class User {
private final String username;
private final UserRole role;
public User(String username, UserRole role) {
this.username = username;
this.role = role;
}
public String getUsername() { return username; }
public UserRole getRole() { return role; }
}
/**
* Protection Proxy - controls access based on user roles
*/
public class DatabaseProxy implements Database {
private final Database realDatabase;
private final User currentUser;
public DatabaseProxy(String databaseName, User user) {
this.realDatabase = new RealDatabase(databaseName);
this.currentUser = user;
System.out.println("Database proxy created for user: " + user.getUsername());
}
private void checkPermission(UserRole requiredRole, String operation) {
if (currentUser.getRole().ordinal() < requiredRole.ordinal()) {
throw new SecurityException(
"User '" + currentUser.getUsername() + "' with role '" + currentUser.getRole() + 
"' is not authorized to perform '" + operation + "'. Required role: " + requiredRole
);
}
}
@Override
public void query(String sql) {
checkPermission(UserRole.GUEST, "QUERY");
System.out.println("Audit: User '" + currentUser.getUsername() + "' executed query: " + sql);
realDatabase.query(sql);
}
@Override
public void update(String sql) {
checkPermission(UserRole.USER, "UPDATE");
System.out.println("Audit: User '" + currentUser.getUsername() + "' executed update: " + sql);
realDatabase.update(sql);
}
@Override
public void delete(String sql) {
checkPermission(UserRole.ADMIN, "DELETE");
System.out.println("Audit: User '" + currentUser.getUsername() + "' executed delete: " + sql);
realDatabase.delete(sql);
}
@Override
public void createTable(String tableName) {
checkPermission(UserRole.DEVELOPER, "CREATE_TABLE");
System.out.println("Audit: User '" + currentUser.getUsername() + "' created table: " + tableName);
realDatabase.createTable(tableName);
}
// Proxy-specific methods
public User getCurrentUser() {
return currentUser;
}
public void changeUser(User newUser) {
System.out.println("Switching user from " + currentUser.getUsername() + " to " + newUser.getUsername());
// In real implementation, we might create a new proxy or handle authentication
}
}

3. Remote Proxy (Network Communication)

/**
* Bank account service interface
*/
public interface BankAccount {
double getBalance();
void deposit(double amount);
void withdraw(double amount) throws InsufficientFundsException;
String getAccountNumber();
String getAccountHolder();
List<Transaction> getTransactionHistory();
}
/**
* Transaction record
*/
public class Transaction {
private final String id;
private final String type;
private final double amount;
private final Date timestamp;
public Transaction(String type, double amount) {
this.id = UUID.randomUUID().toString();
this.type = type;
this.amount = amount;
this.timestamp = new Date();
}
// Getters
public String getId() { return id; }
public String getType() { return type; }
public double getAmount() { return amount; }
public Date getTimestamp() { return timestamp; }
@Override
public String toString() {
return String.format("Transaction[%s: $%.2f at %s]", type, amount, timestamp);
}
}
/**
* Custom exception for banking operations
*/
public class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
/**
* Real bank account implementation (on server side)
*/
public class RealBankAccount implements BankAccount {
private final String accountNumber;
private final String accountHolder;
private double balance;
private final List<Transaction> transactions;
public RealBankAccount(String accountNumber, String accountHolder, double initialBalance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
this.transactions = new ArrayList<>();
transactions.add(new Transaction("OPENING", initialBalance));
}
@Override
public double getBalance() {
return balance;
}
@Override
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
this.balance += amount;
transactions.add(new Transaction("DEPOSIT", amount));
System.out.println("Deposited: $" + amount + " | New balance: $" + balance);
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (amount > balance) {
throw new InsufficientFundsException(
"Insufficient funds. Available: $" + balance + ", Requested: $" + amount
);
}
this.balance -= amount;
transactions.add(new Transaction("WITHDRAWAL", amount));
System.out.println("Withdrawn: $" + amount + " | New balance: $" + balance);
}
@Override
public String getAccountNumber() {
return accountNumber;
}
@Override
public String getAccountHolder() {
return accountHolder;
}
@Override
public List<Transaction> getTransactionHistory() {
return new ArrayList<>(transactions); // Return copy for immutability
}
}
/**
* Remote Proxy - handles network communication transparently
*/
public class BankAccountProxy implements BankAccount {
private final String accountNumber;
private final String serverAddress;
public BankAccountProxy(String accountNumber, String serverAddress) {
this.accountNumber = accountNumber;
this.serverAddress = serverAddress;
System.out.println("Bank account proxy created for account: " + accountNumber);
}
// Simulate network communication
private String sendRequest(String operation, String... parameters) {
System.out.println("Sending request to " + serverAddress + ": " + operation);
// Simulate network delay
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Simulate server response
switch (operation) {
case "GET_BALANCE":
return "1500.75";
case "GET_ACCOUNT_HOLDER":
return "John Doe";
case "GET_TRANSACTIONS":
return "DEPOSIT:500.00,2023-01-15;WITHDRAWAL:200.00,2023-01-20";
default:
return "SUCCESS";
}
}
@Override
public double getBalance() {
String response = sendRequest("GET_BALANCE");
return Double.parseDouble(response);
}
@Override
public void deposit(double amount) {
sendRequest("DEPOSIT", String.valueOf(amount));
System.out.println("Deposit request sent: $" + amount);
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
try {
sendRequest("WITHDRAW", String.valueOf(amount));
System.out.println("Withdrawal request sent: $" + amount);
} catch (Exception e) {
throw new InsufficientFundsException("Withdrawal failed: " + e.getMessage());
}
}
@Override
public String getAccountNumber() {
return accountNumber;
}
@Override
public String getAccountHolder() {
return sendRequest("GET_ACCOUNT_HOLDER");
}
@Override
public List<Transaction> getTransactionHistory() {
String response = sendRequest("GET_TRANSACTIONS");
return parseTransactions(response);
}
private List<Transaction> parseTransactions(String response) {
// Simplified parsing
List<Transaction> transactions = new ArrayList<>();
String[] parts = response.split(";");
for (String part : parts) {
String[] details = part.split(":");
if (details.length == 2) {
String type = details[0];
double amount = Double.parseDouble(details[1]);
transactions.add(new Transaction(type, amount));
}
}
return transactions;
}
}

4. Logging Proxy (Cross-Cutting Concerns)

/**
* Data service interface
*/
public interface DataService {
String fetchData(String query);
void saveData(String data);
void deleteData(String id);
List<String> listAll();
}
/**
* Real data service implementation
*/
public class RealDataService implements DataService {
private final Map<String, String> dataStore = new HashMap<>();
public RealDataService() {
// Initialize with some sample data
dataStore.put("1", "Sample Data 1");
dataStore.put("2", "Sample Data 2");
}
@Override
public String fetchData(String query) {
System.out.println("Fetching data for query: " + query);
// Simulate processing time
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return dataStore.getOrDefault(query, "No data found");
}
@Override
public void saveData(String data) {
System.out.println("Saving data: " + data);
String id = UUID.randomUUID().toString();
dataStore.put(id, data);
}
@Override
public void deleteData(String id) {
System.out.println("Deleting data with id: " + id);
dataStore.remove(id);
}
@Override
public List<String> listAll() {
return new ArrayList<>(dataStore.values());
}
}
/**
* Logging Proxy - adds logging without modifying original class
*/
public class LoggingProxy implements DataService {
private final DataService realService;
private final Logger logger;
public LoggingProxy(DataService realService) {
this.realService = realService;
this.logger = Logger.getLogger(LoggingProxy.class.getName());
}
@Override
public String fetchData(String query) {
long startTime = System.currentTimeMillis();
logger.info("Starting fetchData with query: " + query);
try {
String result = realService.fetchData(query);
long duration = System.currentTimeMillis() - startTime;
logger.info("fetchData completed in " + duration + "ms. Result: " + 
(result != null ? result.substring(0, Math.min(result.length(), 50)) + "..." : "null"));
return result;
} catch (Exception e) {
logger.severe("fetchData failed for query: " + query + " - " + e.getMessage());
throw e;
}
}
@Override
public void saveData(String data) {
logger.info("Starting saveData with data length: " + (data != null ? data.length() : 0));
try {
realService.saveData(data);
logger.info("saveData completed successfully");
} catch (Exception e) {
logger.severe("saveData failed: " + e.getMessage());
throw e;
}
}
@Override
public void deleteData(String id) {
logger.info("Starting deleteData with id: " + id);
try {
realService.deleteData(id);
logger.info("deleteData completed successfully for id: " + id);
} catch (Exception e) {
logger.severe("deleteData failed for id: " + id + " - " + e.getMessage());
throw e;
}
}
@Override
public List<String> listAll() {
logger.info("Starting listAll operation");
try {
List<String> result = realService.listAll();
logger.info("listAll completed. Found " + result.size() + " items");
return result;
} catch (Exception e) {
logger.severe("listAll failed: " + e.getMessage());
throw e;
}
}
}

5. Caching Proxy (Performance Optimization)

/**
* Weather service interface
*/
public interface WeatherService {
WeatherData getWeather(String city);
WeatherData getWeather(double lat, double lon);
List<WeatherData> getWeatherForecast(String city, int days);
}
/**
* Weather data model
*/
public class WeatherData {
private final String location;
private final double temperature;
private final String description;
private final double humidity;
private final double windSpeed;
private final Date timestamp;
public WeatherData(String location, double temperature, String description, 
double humidity, double windSpeed) {
this.location = location;
this.temperature = temperature;
this.description = description;
this.humidity = humidity;
this.windSpeed = windSpeed;
this.timestamp = new Date();
}
// Getters
public String getLocation() { return location; }
public double getTemperature() { return temperature; }
public String getDescription() { return description; }
public double getHumidity() { return humidity; }
public double getWindSpeed() { return windSpeed; }
public Date getTimestamp() { return timestamp; }
@Override
public String toString() {
return String.format("Weather[%s: %.1f°C, %s, Humidity: %.1f%%, Wind: %.1f m/s]", 
location, temperature, description, humidity, windSpeed);
}
}
/**
* Real weather service (makes actual API calls)
*/
public class RealWeatherService implements WeatherService {
@Override
public WeatherData getWeather(String city) {
System.out.println("Making API call to get weather for: " + city);
// Simulate API call delay
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Simulate API response
return new WeatherData(city, 20 + Math.random() * 15, "Sunny", 
40 + Math.random() * 30, 5 + Math.random() * 10);
}
@Override
public WeatherData getWeather(double lat, double lon) {
System.out.println("Making API call to get weather for coordinates: " + lat + ", " + lon);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new WeatherData("Location(" + lat + "," + lon + ")", 
15 + Math.random() * 20, "Cloudy", 
50 + Math.random() * 25, 3 + Math.random() * 8);
}
@Override
public List<WeatherData> getWeatherForecast(String city, int days) {
System.out.println("Making API call to get " + days + "-day forecast for: " + city);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
List<WeatherData> forecast = new ArrayList<>();
for (int i = 0; i < days; i++) {
forecast.add(new WeatherData(city, 18 + Math.random() * 12, 
i % 2 == 0 ? "Sunny" : "Rainy",
45 + Math.random() * 25, 4 + Math.random() * 6));
}
return forecast;
}
}
/**
* Caching Proxy - improves performance by caching results
*/
public class CachingProxy implements WeatherService {
private final WeatherService realService;
private final Map<String, CacheEntry<WeatherData>> cache;
private final long cacheTimeout; // milliseconds
public CachingProxy(WeatherService realService, long cacheTimeout) {
this.realService = realService;
this.cache = new HashMap<>();
this.cacheTimeout = cacheTimeout;
}
private static class CacheEntry<T> {
private final T data;
private final long timestamp;
public CacheEntry(T data) {
this.data = data;
this.timestamp = System.currentTimeMillis();
}
public boolean isExpired(long timeout) {
return System.currentTimeMillis() - timestamp > timeout;
}
public T getData() { return data; }
}
@Override
public WeatherData getWeather(String city) {
String cacheKey = "weather:" + city.toLowerCase();
CacheEntry<WeatherData> cached = cache.get(cacheKey);
if (cached != null && !cached.isExpired(cacheTimeout)) {
System.out.println("Returning cached weather for: " + city);
return cached.getData();
}
System.out.println("Cache miss for: " + city + ", fetching from service...");
WeatherData weather = realService.getWeather(city);
cache.put(cacheKey, new CacheEntry<>(weather));
return weather;
}
@Override
public WeatherData getWeather(double lat, double lon) {
String cacheKey = String.format("weather:%.4f,%.4f", lat, lon);
CacheEntry<WeatherData> cached = cache.get(cacheKey);
if (cached != null && !cached.isExpired(cacheTimeout)) {
System.out.println("Returning cached weather for coordinates");
return cached.getData();
}
System.out.println("Cache miss for coordinates, fetching from service...");
WeatherData weather = realService.getWeather(lat, lon);
cache.put(cacheKey, new CacheEntry<>(weather));
return weather;
}
@Override
public List<WeatherData> getWeatherForecast(String city, int days) {
String cacheKey = "forecast:" + city.toLowerCase() + ":" + days;
@SuppressWarnings("unchecked")
CacheEntry<List<WeatherData>> cached = (CacheEntry<List<WeatherData>>) cache.get(cacheKey);
if (cached != null && !cached.isExpired(cacheTimeout)) {
System.out.println("Returning cached forecast for: " + city);
return cached.getData();
}
System.out.println("Cache miss for forecast, fetching from service...");
List<WeatherData> forecast = realService.getWeatherForecast(city, days);
cache.put(cacheKey, new CacheEntry<>(forecast));
return forecast;
}
// Cache management methods
public void clearCache() {
cache.clear();
System.out.println("Cache cleared");
}
public int getCacheSize() {
return cache.size();
}
public void removeExpiredEntries() {
int initialSize = cache.size();
cache.entrySet().removeIf(entry -> entry.getValue().isExpired(cacheTimeout));
int removed = initialSize - cache.size();
System.out.println("Removed " + removed + " expired cache entries");
}
}

Dynamic Proxies (Java Reflection)

6. Dynamic Invocation Handler

/**
* Generic dynamic proxy for method timing
*/
public class TimingDynamicProxy implements InvocationHandler {
private final Object target;
private final Map<String, MethodStats> methodStats;
public TimingDynamicProxy(Object target) {
this.target = target;
this.methodStats = new ConcurrentHashMap<>();
}
public static <T> T createProxy(T target, Class<T> interfaceClass) {
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class<?>[] { interfaceClass },
new TimingDynamicProxy(target)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = System.nanoTime();
String methodName = method.getName();
try {
Object result = method.invoke(target, args);
recordSuccess(methodName, System.nanoTime() - startTime);
return result;
} catch (InvocationTargetException e) {
recordFailure(methodName, System.nanoTime() - startTime);
throw e.getTargetException();
}
}
private void recordSuccess(String methodName, long duration) {
MethodStats stats = methodStats.computeIfAbsent(methodName, k -> new MethodStats());
stats.recordSuccess(duration);
}
private void recordFailure(String methodName, long duration) {
MethodStats stats = methodStats.computeIfAbsent(methodName, k -> new MethodStats());
stats.recordFailure(duration);
}
public void printStatistics() {
System.out.println("\n=== Method Timing Statistics ===");
methodStats.forEach((methodName, stats) -> {
System.out.printf("%s: %d calls, avg: %.2f ms, success: %d, failures: %d%n",
methodName, stats.getTotalCalls(), 
stats.getAverageTime() / 1_000_000.0,
stats.getSuccessCount(), stats.getFailureCount());
});
}
private static class MethodStats {
private long totalCalls = 0;
private long successCount = 0;
private long failureCount = 0;
private long totalTime = 0;
public void recordSuccess(long duration) {
totalCalls++;
successCount++;
totalTime += duration;
}
public void recordFailure(long duration) {
totalCalls++;
failureCount++;
totalTime += duration;
}
// Getters
public long getTotalCalls() { return totalCalls; }
public long getSuccessCount() { return successCount; }
public long getFailureCount() { return failureCount; }
public double getAverageTime() { 
return totalCalls > 0 ? (double) totalTime / totalCalls : 0; 
}
}
}
/**
* Service interface for dynamic proxy demonstration
*/
public interface UserService {
User createUser(String username, String email);
User findUserById(String id);
void deleteUser(String id);
List<User> listUsers();
}
/**
* User entity
*/
public class User {
private final String id;
private final String username;
private final String email;
private final Date createdAt;
public User(String username, String email) {
this.id = UUID.randomUUID().toString();
this.username = username;
this.email = email;
this.createdAt = new Date();
}
// Getters
public String getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
public Date getCreatedAt() { return createdAt; }
@Override
public String toString() {
return String.format("User[%s: %s, %s]", id, username, email);
}
}

Comprehensive Example: E-commerce System

7. Complete E-commerce Proxy System

/**
* Payment service interface
*/
public interface PaymentService {
PaymentResult processPayment(PaymentRequest request) throws PaymentException;
PaymentStatus checkPaymentStatus(String paymentId);
RefundResult processRefund(String paymentId, double amount) throws RefundException;
}
/**
* Payment models
*/
public class PaymentRequest {
private final String orderId;
private final double amount;
private final String currency;
private final String cardNumber;
private final String cardHolder;
public PaymentRequest(String orderId, double amount, String currency, 
String cardNumber, String cardHolder) {
this.orderId = orderId;
this.amount = amount;
this.currency = currency;
this.cardNumber = cardNumber;
this.cardHolder = cardHolder;
}
// Getters
public String getOrderId() { return orderId; }
public double getAmount() { return amount; }
public String getCurrency() { return currency; }
public String getCardNumber() { return cardNumber; }
public String getCardHolder() { return cardHolder; }
}
public class PaymentResult {
private final String paymentId;
private final boolean success;
private final String message;
private final Date processedAt;
public PaymentResult(String paymentId, boolean success, String message) {
this.paymentId = paymentId;
this.success = success;
this.message = message;
this.processedAt = new Date();
}
// Getters
public String getPaymentId() { return paymentId; }
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public Date getProcessedAt() { return processedAt; }
}
public enum PaymentStatus {
PENDING, SUCCESS, FAILED, REFUNDED
}
public class RefundResult {
private final String refundId;
private final boolean success;
private final String message;
public RefundResult(String refundId, boolean success, String message) {
this.refundId = refundId;
this.success = success;
this.message = message;
}
// Getters
public String getRefundId() { return refundId; }
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
}
/**
* Custom exceptions
*/
public class PaymentException extends Exception {
public PaymentException(String message) { super(message); }
public PaymentException(String message, Throwable cause) { super(message, cause); }
}
public class RefundException extends Exception {
public RefundException(String message) { super(message); }
public RefundException(String message, Throwable cause) { super(message, cause); }
}
/**
* Real payment service (connects to actual payment gateway)
*/
public class RealPaymentService implements PaymentService {
@Override
public PaymentResult processPayment(PaymentRequest request) throws PaymentException {
System.out.println("Processing payment for order: " + request.getOrderId());
// Simulate payment processing
try {
Thread.sleep(1000); // Simulate network delay
// Simulate random failures for demonstration
if (Math.random() < 0.1) { // 10% failure rate
throw new PaymentException("Payment gateway timeout");
}
if (request.getAmount() <= 0) {
throw new PaymentException("Invalid amount: " + request.getAmount());
}
String paymentId = "PAY-" + UUID.randomUUID().toString().substring(0, 8);
return new PaymentResult(paymentId, true, "Payment processed successfully");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new PaymentException("Payment processing interrupted", e);
}
}
@Override
public PaymentStatus checkPaymentStatus(String paymentId) {
System.out.println("Checking payment status for: " + paymentId);
// Simulate status check
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Return random status for demonstration
PaymentStatus[] statuses = PaymentStatus.values();
return statuses[(int) (Math.random() * statuses.length)];
}
@Override
public RefundResult processRefund(String paymentId, double amount) throws RefundException {
System.out.println("Processing refund for payment: " + paymentId);
try {
Thread.sleep(800);
if (amount <= 0) {
throw new RefundException("Invalid refund amount: " + amount);
}
String refundId = "REF-" + UUID.randomUUID().toString().substring(0, 8);
return new RefundResult(refundId, true, "Refund processed successfully");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RefundException("Refund processing interrupted", e);
}
}
}
/**
* Comprehensive Payment Service Proxy with multiple concerns
*/
public class ComprehensivePaymentProxy implements PaymentService {
private final PaymentService realService;
private final Map<String, PaymentResult> paymentCache;
private final Map<String, PaymentStatus> statusCache;
private final CircuitBreaker circuitBreaker;
private final RateLimiter rateLimiter;
public ComprehensivePaymentProxy(PaymentService realService) {
this.realService = realService;
this.paymentCache = new ConcurrentHashMap<>();
this.statusCache = new ConcurrentHashMap<>();
this.circuitBreaker = new CircuitBreaker(5, 30000); // 5 failures, 30s timeout
this.rateLimiter = new RateLimiter(10, 60000); // 10 requests per minute
}
@Override
public PaymentResult processPayment(PaymentRequest request) throws PaymentException {
// 1. Rate limiting
if (!rateLimiter.allowRequest()) {
throw new PaymentException("Rate limit exceeded. Please try again later.");
}
// 2. Circuit breaker check
if (!circuitBreaker.allowRequest()) {
throw new PaymentException("Payment service is temporarily unavailable");
}
// 3. Input validation
validatePaymentRequest(request);
String cacheKey = "payment:" + request.getOrderId();
// 4. Check cache
PaymentResult cachedResult = paymentCache.get(cacheKey);
if (cachedResult != null) {
System.out.println("Returning cached payment result for order: " + request.getOrderId());
return cachedResult;
}
// 5. Process with circuit breaker
try {
PaymentResult result = realService.processPayment(request);
// 6. Cache successful payments
if (result.isSuccess()) {
paymentCache.put(cacheKey, result);
statusCache.put(result.getPaymentId(), PaymentStatus.SUCCESS);
}
circuitBreaker.recordSuccess();
return result;
} catch (PaymentException e) {
circuitBreaker.recordFailure();
throw e;
} catch (Exception e) {
circuitBreaker.recordFailure();
throw new PaymentException("Unexpected error during payment processing", e);
}
}
@Override
public PaymentStatus checkPaymentStatus(String paymentId) {
// 1. Check cache first
PaymentStatus cachedStatus = statusCache.get(paymentId);
if (cachedStatus != null) {
System.out.println("Returning cached status for payment: " + paymentId);
return cachedStatus;
}
// 2. Rate limiting for status checks (more lenient)
if (!rateLimiter.allowRequest()) {
return PaymentStatus.PENDING; // Default to pending if rate limited
}
// 3. Circuit breaker for status checks
if (!circuitBreaker.allowRequest()) {
return PaymentStatus.PENDING;
}
try {
PaymentStatus status = realService.checkPaymentStatus(paymentId);
// 4. Cache the status
statusCache.put(paymentId, status);
circuitBreaker.recordSuccess();
return status;
} catch (Exception e) {
circuitBreaker.recordFailure();
return PaymentStatus.PENDING; // Default to pending on error
}
}
@Override
public RefundResult processRefund(String paymentId, double amount) throws RefundException {
// Similar comprehensive handling for refunds
if (!rateLimiter.allowRequest()) {
throw new RefundException("Rate limit exceeded");
}
if (!circuitBreaker.allowRequest()) {
throw new RefundException("Service temporarily unavailable");
}
try {
RefundResult result = realService.processRefund(paymentId, amount);
// Update cache if refund is successful
if (result.isSuccess()) {
statusCache.put(paymentId, PaymentStatus.REFUNDED);
}
circuitBreaker.recordSuccess();
return result;
} catch (RefundException e) {
circuitBreaker.recordFailure();
throw e;
} catch (Exception e) {
circuitBreaker.recordFailure();
throw new RefundException("Unexpected error during refund processing", e);
}
}
private void validatePaymentRequest(PaymentRequest request) throws PaymentException {
if (request == null) {
throw new PaymentException("Payment request cannot be null");
}
if (request.getOrderId() == null || request.getOrderId().trim().isEmpty()) {
throw new PaymentException("Order ID is required");
}
if (request.getAmount() <= 0) {
throw new PaymentException("Amount must be positive");
}
if (request.getCardNumber() == null || request.getCardNumber().trim().isEmpty()) {
throw new PaymentException("Card number is required");
}
if (!isValidCardNumber(request.getCardNumber())) {
throw new PaymentException("Invalid card number");
}
}
private boolean isValidCardNumber(String cardNumber) {
// Simple validation for demonstration
String cleaned = cardNumber.replaceAll("\\s+", "");
return cleaned.length() >= 13 && cleaned.length() <= 19 && 
cleaned.matches("\\d+");
}
// Proxy management methods
public void clearCache() {
paymentCache.clear();
statusCache.clear();
System.out.println("Payment cache cleared");
}
public CircuitBreaker.State getCircuitBreakerState() {
return circuitBreaker.getState();
}
public int getCacheSize() {
return paymentCache.size() + statusCache.size();
}
}
/**
* Supporting classes for comprehensive proxy
*/
class CircuitBreaker {
public enum State { CLOSED, OPEN, HALF_OPEN }
private State state = State.CLOSED;
private int failureCount = 0;
private final int failureThreshold;
private final long timeout;
private long lastFailureTime;
public CircuitBreaker(int failureThreshold, long timeout) {
this.failureThreshold = failureThreshold;
this.timeout = timeout;
}
public synchronized boolean allowRequest() {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > timeout) {
state = State.HALF_OPEN;
return true;
}
return false;
}
return true;
}
public synchronized void recordSuccess() {
failureCount = 0;
state = State.CLOSED;
}
public synchronized void recordFailure() {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= failureThreshold) {
state = State.OPEN;
}
}
public State getState() {
return state;
}
}
class RateLimiter {
private final int maxRequests;
private final long timeWindow;
private final Queue<Long> requestTimes;
public RateLimiter(int maxRequests, long timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requestTimes = new LinkedList<>();
}
public synchronized boolean allowRequest() {
long currentTime = System.currentTimeMillis();
// Remove old requests outside the time window
while (!requestTimes.isEmpty() && 
currentTime - requestTimes.peek() > timeWindow) {
requestTimes.poll();
}
// Check if within rate limit
if (requestTimes.size() < maxRequests) {
requestTimes.offer(currentTime);
return true;
}
return false;
}
}

Testing the Proxy Patterns

Comprehensive Test Suite

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Proxy;
import java.util.List;
public class ProxyPatternTest {
@Test
public void testVirtualProxy() {
System.out.println("\n=== Testing Virtual Proxy ===");
Image image = new ImageProxy("large_photo.jpg");
System.out.println("Image proxy created, real image not loaded yet");
assertFalse(((ImageProxy) image).isImageLoaded());
// Real image loaded only when needed
image.display();
assertTrue(((ImageProxy) image).isImageLoaded());
}
@Test
public void testProtectionProxy() {
System.out.println("\n=== Testing Protection Proxy ===");
User guest = new User("guest", UserRole.GUEST);
User admin = new User("admin", UserRole.ADMIN);
Database guestDb = new DatabaseProxy("test_db", guest);
Database adminDb = new DatabaseProxy("test_db", admin);
// Guest can query
guestDb.query("SELECT * FROM users");
// Guest cannot update
assertThrows(SecurityException.class, () -> {
guestDb.update("UPDATE users SET name='test'");
});
// Admin can update
adminDb.update("UPDATE users SET name='test'");
}
@Test
public void testCachingProxy() {
System.out.println("\n=== Testing Caching Proxy ===");
WeatherService realService = new RealWeatherService();
WeatherService cachingService = new CachingProxy(realService, 5000); // 5 second cache
// First call - should call real service
WeatherData weather1 = cachingService.getWeather("London");
// Second call - should return from cache
WeatherData weather2 = cachingService.getWeather("London");
assertEquals(weather1.getLocation(), weather2.getLocation());
// Verify cache is working
assertEquals(1, ((CachingProxy) cachingService).getCacheSize());
}
@Test
public void testLoggingProxy() {
System.out.println("\n=== Testing Logging Proxy ===");
DataService realService = new RealDataService();
DataService loggingService = new LoggingProxy(realService);
String result = loggingService.fetchData("1");
assertNotNull(result);
loggingService.saveData("New data");
loggingService.listAll();
}
@Test
public void testDynamicProxy() {
System.out.println("\n=== Testing Dynamic Proxy ===");
UserService realService = new UserService() {
@Override
public User createUser(String username, String email) {
return new User(username, email);
}
@Override
public User findUserById(String id) {
return new User("test", "[email protected]");
}
@Override
public void deleteUser(String id) {
System.out.println("Deleting user: " + id);
}
@Override
public List<User> listUsers() {
return List.of(new User("user1", "[email protected]"));
}
};
UserService proxiedService = TimingDynamicProxy.createProxy(realService, UserService.class);
User user = proxiedService.createUser("john", "[email protected]");
assertNotNull(user);
proxiedService.findUserById("123");
proxiedService.listUsers();
// Print timing statistics
((TimingDynamicProxy) Proxy.getInvocationHandler(proxiedService)).printStatistics();
}
@Test
public void testComprehensivePaymentProxy() throws PaymentException {
System.out.println("\n=== Testing Comprehensive Payment Proxy ===");
PaymentService realService = new RealPaymentService();
ComprehensivePaymentProxy paymentProxy = new ComprehensivePaymentProxy(realService);
PaymentRequest request = new PaymentRequest(
"ORDER-123", 100.0, "USD", "4111111111111111", "John Doe"
);
PaymentResult result = paymentProxy.processPayment(request);
assertTrue(result.isSuccess());
// Check cache
assertEquals(1, paymentProxy.getCacheSize());
// Check status (should be cached)
PaymentStatus status = paymentProxy.checkPaymentStatus(result.getPaymentId());
assertNotNull(status);
}
}

Best Practices and Considerations

When to Use Proxy Pattern

public class ProxyBestPractices {
/**
* Appropriate use cases for Proxy Pattern:
* 
* 1. Lazy Initialization (Virtual Proxy)
*    - When object creation is expensive
*    - When object might not be used immediately
*    
* 2. Access Control (Protection Proxy)
*    - When you need to control access to sensitive operations
*    - When you need different access levels for different users
*    
* 3. Remote Communication (Remote Proxy)
*    - When dealing with remote services or distributed systems
*    - When you want to hide network complexity
*    
* 4. Caching (Cache Proxy)
*    - When you want to cache expensive operations
*    - When you need to reduce network calls or computation
*    
* 5. Logging and Monitoring (Logging Proxy)
*    - When you need to add cross-cutting concerns
*    - When you want to monitor method calls and performance
*    
* 6. Circuit Breaker and Resilience
*    - When you need to add fault tolerance
*    - When dealing with unreliable external services
*/
// Good example: Service with multiple concerns
public interface ExternalService {
Data fetchData(String id);
void updateData(String id, Data data);
}
public class ComprehensiveServiceProxy implements ExternalService {
private final ExternalService realService;
private final Cache cache;
private final CircuitBreaker circuitBreaker;
private final Logger logger;
public ComprehensiveServiceProxy(ExternalService realService) {
this.realService = realService;
this.cache = new Cache();
this.circuitBreaker = new CircuitBreaker();
this.logger = Logger.getLogger(getClass().getName());
}
@Override
public Data fetchData(String id) {
// 1. Check cache
Data cached = cache.get(id);
if (cached != null) {
logger.info("Returning cached data for: " + id);
return cached;
}
// 2. Circuit breaker check
if (!circuitBreaker.allowRequest()) {
throw new ServiceUnavailableException("Service temporarily unavailable");
}
// 3. Logging
logger.info("Fetching data for: " + id);
long startTime = System.currentTimeMillis();
try {
Data result = realService.fetchData(id);
// 4. Cache successful results
cache.put(id, result);
// 5. Record success
circuitBreaker.recordSuccess();
logger.info("Data fetched successfully in " + 
(System.currentTimeMillis() - startTime) + "ms");
return result;
} catch (Exception e) {
// 6. Record failure
circuitBreaker.recordFailure();
logger.error("Failed to fetch data for: " + id, e);
throw e;
}
}
@Override
public void updateData(String id, Data data) {
// Similar comprehensive handling for updates
// Invalidate cache, handle failures, etc.
}
}
}
/**
* Considerations and Anti-patterns:
* 
* 1. Don't use proxy when simple composition would suffice
* 2. Be careful with deep proxy chains (performance impact)
* 3. Ensure proxy doesn't change the expected behavior
* 4. Consider using frameworks (Spring AOP) for cross-cutting concerns
* 5. Be mindful of serialization issues with proxies
*/
public class ProxyConsiderations {
// Anti-pattern: Overusing proxies for simple tasks
public class OverEngineeredProxy implements SimpleService {
private final SimpleService realService;
private final List<MethodInterceptor> interceptors;
// Too complex for simple service
}
// Better: Use proxy only when you have clear benefits
public class SimpleServiceProxy implements SimpleService {
private final SimpleService realService;
public SimpleServiceProxy(SimpleService realService) {
this.realService = realService;
}
@Override
public String getData() {
// Add only necessary functionality
return realService.getData();
}
}
}

Conclusion

The Proxy pattern is a powerful structural pattern that provides controlled access to objects. Key benefits include:

  • Lazy initialization for expensive objects
  • Access control and security enforcement
  • Performance optimization through caching
  • Cross-cutting concerns without modifying original code
  • Remote communication abstraction
  • Resilience patterns implementation

The pattern is particularly valuable in distributed systems, security-sensitive applications, and performance-critical scenarios where you need to add functionality without changing the core business logic.

Leave a Reply

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


Macro Nepal Helper