Proxy Pattern - Providing Placeholder or Surrogate for Another Object

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand different types of proxies and their specific use cases
  • Implement virtual, protection, and remote proxies effectively
  • Apply proxy pattern for lazy loading, access control, and caching
  • Design transparent proxies that maintain interface compatibility
  • Recognize proxy usage in modern frameworks and libraries

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about using an ATM to access your bank account. The ATM isn’t your actual bank account, but it provides the same interface and controls access to your real account. It might cache some information, control what operations you can perform, and handle the communication with the bank’s servers. How is this similar to object interactions in software?

Lesson Overview

  • Duration: 120 minutes
  • Prerequisites: Object-oriented programming, interfaces, basic understanding of networking concepts
  • Materials Needed: Development environment with Java, Python, or JavaScript support

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Proxy Types - Virtual, protection, remote, and smart reference proxies
  2. Implementation Patterns - Static vs dynamic proxies, decorator-style vs inheritance
  3. Lazy Loading - Deferring expensive object creation and initialization
  4. Access Control - Permission checking and security enforcement
  5. Caching and Optimization - Result caching and performance improvements
  6. Framework Integration - How proxies are used in modern frameworks

Success Criteria:

By the end of this session, you should be able to implement different proxy types and choose the appropriate proxy pattern for specific scenarios.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

What is the Proxy Pattern?

The Proxy pattern provides a placeholder or surrogate for another object to control access to it. The proxy acts as an intermediary that can add functionality like access control, lazy loading, caching, or logging without changing the original object.

Real-World Analogy: A proxy is like a personal assistant who manages access to a busy executive. The assistant can:

  • Screen calls and appointments (access control)
  • Provide cached information without bothering the executive (caching)
  • Schedule meetings for later when the executive is available (lazy loading)
  • Log all interactions for security purposes (logging)

Why This Matters: Proxies allow you to add cross-cutting concerns (security, logging, caching) without modifying the original object, supporting the Single Responsibility Principle and Open/Closed Principle.

Types of Proxies

Virtual Proxy:

  • Defers expensive object creation until actually needed
  • Creates placeholder that loads real object on first access
  • Common for large images, database connections, heavy computations

Protection Proxy:

  • Controls access to the original object
  • Implements permission checking and authorization
  • Can restrict method calls based on user roles or conditions

Remote Proxy:

  • Represents objects in different address spaces
  • Handles network communication and serialization
  • Makes remote objects appear local

Smart Reference:

  • Adds additional behavior when objects are accessed
  • Can implement reference counting, logging, thread safety
  • Manages object lifecycle and cleanup

Key Terminology

  • Subject: Common interface implemented by both proxy and real object
  • Proxy: Maintains reference to real object and controls access to it
  • Real Subject: The actual object that proxy represents
  • Client: Uses objects through the Subject interface
  • Transparent Proxy: Client doesn’t know it’s using a proxy

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Proxy Pattern Structure

The Proxy pattern is essential when:

  • You need to control access to an object
  • You want to add functionality without modifying the original object
  • Object creation is expensive and should be deferred
  • You need to work with remote objects transparently
  • You want to implement cross-cutting concerns like logging or caching

Practical Applications

Proxy pattern is commonly used for:

  • ORM Frameworks: Hibernate lazy loading, JPA proxy entities
  • Web Frameworks: Spring AOP, method interceptors, transaction management
  • Security Systems: Access control, authentication, authorization
  • Caching Systems: Result caching, memoization, CDN proxies
  • Remote Communication: RPC stubs, web service clients, distributed objects
  • Resource Management: Connection pooling, expensive object lifecycle

Common Misconceptions

  1. Misconception: Proxy is the same as Decorator pattern Reality: Proxy controls access to an object; Decorator adds behavior to an object

  2. Misconception: Proxies always add overhead Reality: Proxies can improve performance through caching and lazy loading

5. Live Coding Demonstration (20-25 minutes)

Example 1: Virtual Proxy for Expensive Object Creation

// Subject interface
interface Image {
    void display();
    int getWidth();
    int getHeight();
    String getFormat();
}

// Expensive real subject
class HighResolutionImage implements Image {
    private String filename;
    private int width;
    private int height;
    private String format;
    private byte[] imageData;
    
    public HighResolutionImage(String filename) {
        this.filename = filename;
        loadImage(); // Expensive operation
    }
    
    private void loadImage() {
        System.out.println("Loading expensive high-resolution image: " + filename);
        // Simulate expensive image loading
        try {
            Thread.sleep(2000); // 2 seconds to load
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        // Simulate image processing
        this.width = 4096;
        this.height = 3072;
        this.format = "RAW";
        this.imageData = new byte[width * height * 4]; // Large image data
        
        System.out.println("Image loaded: " + filename + " (" + width + "x" + height + ")");
    }
    
    @Override
    public void display() {
        System.out.println("Displaying high-resolution image: " + filename);
        // Simulate complex rendering
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    @Override
    public int getWidth() { return width; }
    
    @Override
    public int getHeight() { return height; }
    
    @Override
    public String getFormat() { return format; }
}

// Virtual Proxy
class ImageProxy implements Image {
    private String filename;
    private HighResolutionImage realImage;
    private boolean isLoaded = false;
    
    // Basic metadata available without loading full image
    private int thumbnailWidth = 256;
    private int thumbnailHeight = 192;
    
    public ImageProxy(String filename) {
        this.filename = filename;
        System.out.println("ImageProxy created for: " + filename);
    }
    
    @Override
    public void display() {
        if (!isLoaded) {
            System.out.println("First access - loading real image...");
            realImage = new HighResolutionImage(filename);
            isLoaded = true;
        }
        realImage.display();
    }
    
    @Override
    public int getWidth() {
        // Can return cached or estimated values without loading full image
        if (!isLoaded) {
            System.out.println("Returning estimated width without loading full image");
            return thumbnailWidth * 16; // Estimated based on thumbnail
        }
        return realImage.getWidth();
    }
    
    @Override
    public int getHeight() {
        if (!isLoaded) {
            System.out.println("Returning estimated height without loading full image");
            return thumbnailHeight * 16;
        }
        return realImage.getHeight();
    }
    
    @Override
    public String getFormat() {
        if (!isLoaded) {
            return "Unknown"; // Could read from file header without full load
        }
        return realImage.getFormat();
    }
}

Example 2: Protection Proxy for Access Control

// Subject interface
interface BankAccount {
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance();
    List<String> getTransactionHistory();
}

// Real subject
class RealBankAccount implements BankAccount {
    private String accountNumber;
    private double balance;
    private List<String> transactions;
    
    public RealBankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
        this.transactions = new ArrayList<>();
        transactions.add("Account opened with balance: $" + initialBalance);
    }
    
    @Override
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            transactions.add("Deposited: $" + amount + " (Balance: $" + balance + ")");
            System.out.println("Deposited $" + amount + ". New balance: $" + balance);
        }
    }
    
    @Override
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            transactions.add("Withdrew: $" + amount + " (Balance: $" + balance + ")");
            System.out.println("Withdrew $" + amount + ". New balance: $" + balance);
        } else {
            System.out.println("Insufficient funds or invalid amount");
        }
    }
    
    @Override
    public double getBalance() {
        return balance;
    }
    
    @Override
    public List<String> getTransactionHistory() {
        return new ArrayList<>(transactions);
    }
}

// User context for access control
class UserContext {
    private String username;
    private Set<String> roles;
    private boolean isAuthenticated;
    
    public UserContext(String username, Set<String> roles, boolean isAuthenticated) {
        this.username = username;
        this.roles = roles;
        this.isAuthenticated = isAuthenticated;
    }
    
    public String getUsername() { return username; }
    public Set<String> getRoles() { return roles; }
    public boolean isAuthenticated() { return isAuthenticated; }
    public boolean hasRole(String role) { return roles.contains(role); }
}

// Protection Proxy
class ProtectedBankAccount implements BankAccount {
    private RealBankAccount realAccount;
    private UserContext userContext;
    private double dailyWithdrawLimit = 1000.0;
    private double totalWithdrawnToday = 0.0;
    
    public ProtectedBankAccount(RealBankAccount realAccount, UserContext userContext) {
        this.realAccount = realAccount;
        this.userContext = userContext;
    }
    
    @Override
    public void deposit(double amount) {
        if (!userContext.isAuthenticated()) {
            throw new SecurityException("User not authenticated");
        }
        
        if (amount > 10000 && !userContext.hasRole("HIGH_VALUE_TRANSACTIONS")) {
            throw new SecurityException("Large deposit requires special authorization");
        }
        
        System.out.println("Access granted for deposit by: " + userContext.getUsername());
        realAccount.deposit(amount);
    }
    
    @Override
    public void withdraw(double amount) {
        if (!userContext.isAuthenticated()) {
            throw new SecurityException("User not authenticated");
        }
        
        if (!userContext.hasRole("WITHDRAW")) {
            throw new SecurityException("User not authorized to withdraw");
        }
        
        if (totalWithdrawnToday + amount > dailyWithdrawLimit) {
            throw new SecurityException("Daily withdrawal limit exceeded");
        }
        
        System.out.println("Access granted for withdrawal by: " + userContext.getUsername());
        realAccount.withdraw(amount);
        totalWithdrawnToday += amount;
    }
    
    @Override
    public double getBalance() {
        if (!userContext.isAuthenticated()) {
            throw new SecurityException("User not authenticated");
        }
        
        if (!userContext.hasRole("VIEW_BALANCE")) {
            throw new SecurityException("User not authorized to view balance");
        }
        
        return realAccount.getBalance();
    }
    
    @Override
    public List<String> getTransactionHistory() {
        if (!userContext.isAuthenticated()) {
            throw new SecurityException("User not authenticated");
        }
        
        if (!userContext.hasRole("VIEW_HISTORY")) {
            throw new SecurityException("User not authorized to view transaction history");
        }
        
        System.out.println("Providing transaction history to: " + userContext.getUsername());
        return realAccount.getTransactionHistory();
    }
}

Example 3: Caching Proxy for Performance

// Subject interface
interface DataService {
    String getData(String key);
    void putData(String key, String value);
    List<String> searchData(String query);
}

// Expensive real subject (e.g., database service)
class DatabaseService implements DataService {
    private Map<String, String> database = new HashMap<>();
    
    public DatabaseService() {
        // Pre-populate with some data
        database.put("user1", "John Doe");
        database.put("user2", "Jane Smith");
        database.put("product1", "Laptop Computer");
        database.put("product2", "Wireless Mouse");
    }
    
    @Override
    public String getData(String key) {
        System.out.println("Database query for key: " + key);
        // Simulate expensive database operation
        try {
            Thread.sleep(100); // 100ms database latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        return database.get(key);
    }
    
    @Override
    public void putData(String key, String value) {
        System.out.println("Database write for key: " + key);
        // Simulate expensive write operation
        try {
            Thread.sleep(200); // 200ms write latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        database.put(key, value);
    }
    
    @Override
    public List<String> searchData(String query) {
        System.out.println("Database search for query: " + query);
        // Simulate expensive search
        try {
            Thread.sleep(500); // 500ms search latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        return database.values().stream()
                .filter(value -> value.toLowerCase().contains(query.toLowerCase()))
                .collect(Collectors.toList());
    }
}

// Caching Proxy
class CachingDataService implements DataService {
    private DataService realService;
    private Map<String, String> cache = new HashMap<>();
    private Map<String, List<String>> searchCache = new HashMap<>();
    private Map<String, Long> cacheTimestamps = new HashMap<>();
    private Map<String, Long> searchTimestamps = new HashMap<>();
    private long cacheExpirationTime = 5000; // 5 seconds
    
    public CachingDataService(DataService realService) {
        this.realService = realService;
    }
    
    @Override
    public String getData(String key) {
        // Check if we have cached data that's still valid
        if (cache.containsKey(key)) {
            long timestamp = cacheTimestamps.get(key);
            if (System.currentTimeMillis() - timestamp < cacheExpirationTime) {
                System.out.println("Cache hit for key: " + key);
                return cache.get(key);
            } else {
                System.out.println("Cache expired for key: " + key);
                cache.remove(key);
                cacheTimestamps.remove(key);
            }
        }
        
        System.out.println("Cache miss for key: " + key);
        String value = realService.getData(key);
        
        // Cache the result
        if (value != null) {
            cache.put(key, value);
            cacheTimestamps.put(key, System.currentTimeMillis());
        }
        
        return value;
    }
    
    @Override
    public void putData(String key, String value) {
        // Write through to real service
        realService.putData(key, value);
        
        // Update cache
        cache.put(key, value);
        cacheTimestamps.put(key, System.currentTimeMillis());
        
        // Invalidate related search results
        searchCache.clear();
        searchTimestamps.clear();
    }
    
    @Override
    public List<String> searchData(String query) {
        // Check search cache
        if (searchCache.containsKey(query)) {
            long timestamp = searchTimestamps.get(query);
            if (System.currentTimeMillis() - timestamp < cacheExpirationTime) {
                System.out.println("Search cache hit for query: " + query);
                return new ArrayList<>(searchCache.get(query));
            } else {
                System.out.println("Search cache expired for query: " + query);
                searchCache.remove(query);
                searchTimestamps.remove(query);
            }
        }
        
        System.out.println("Search cache miss for query: " + query);
        List<String> results = realService.searchData(query);
        
        // Cache the search results
        searchCache.put(query, new ArrayList<>(results));
        searchTimestamps.put(query, System.currentTimeMillis());
        
        return results;
    }
    
    public void clearCache() {
        cache.clear();
        cacheTimestamps.clear();
        searchCache.clear();
        searchTimestamps.clear();
        System.out.println("Cache cleared");
    }
}

Example 4: Dynamic Proxy (Java)

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// Service interface
interface UserService {
    User findUser(String id);
    void updateUser(User user);
    void deleteUser(String id);
}

// Real service implementation
class UserServiceImpl implements UserService {
    @Override
    public User findUser(String id) {
        System.out.println("Finding user: " + id);
        return new User(id, "User " + id);
    }
    
    @Override
    public void updateUser(User user) {
        System.out.println("Updating user: " + user.getId());
    }
    
    @Override
    public void deleteUser(String id) {
        System.out.println("Deleting user: " + id);
    }
}

// Dynamic proxy handler
class LoggingInvocationHandler implements InvocationHandler {
    private Object target;
    
    public LoggingInvocationHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        System.out.println("Before method: " + method.getName());
        System.out.println("Arguments: " + Arrays.toString(args));
        
        try {
            Object result = method.invoke(target, args);
            
            long endTime = System.currentTimeMillis();
            System.out.println("After method: " + method.getName());
            System.out.println("Execution time: " + (endTime - startTime) + "ms");
            System.out.println("Result: " + result);
            
            return result;
        } catch (Exception e) {
            System.out.println("Exception in method: " + method.getName());
            System.out.println("Exception: " + e.getMessage());
            throw e;
        }
    }
}

// Factory for creating dynamic proxies
class ProxyFactory {
    public static <T> T createLoggingProxy(T target, Class<T> interfaceType) {
        return (T) Proxy.newProxyInstance(
            interfaceType.getClassLoader(),
            new Class[]{interfaceType},
            new LoggingInvocationHandler(target)
        );
    }
}

Debugging Demonstration

// Common mistake - forgetting to delegate to real object
public class BadProxy implements Image {
    private Image realImage;
    
    @Override
    public void display() {
        System.out.println("Proxy display");
        // FORGOT TO CALL: realImage.display();
    }
}

// Correct implementation
public class GoodProxy implements Image {
    private Image realImage;
    
    @Override
    public void display() {
        if (realImage == null) {
            realImage = new HighResolutionImage("image.jpg");
        }
        System.out.println("Proxy display");
        realImage.display(); // Remember to delegate!
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Basic Practice (10 minutes)

Objective: Implement a virtual proxy for expensive file operations

Instructions:

  1. Create a FileProcessor interface with methods: processFile(), getFileSize(), getFileType()
  2. Implement an expensive RealFileProcessor that simulates slow file operations
  3. Create a FileProcessorProxy that defers actual processing until needed
  4. Test that file metadata can be accessed quickly without processing

Starter Code:

interface FileProcessor {
    void processFile();
    long getFileSize();
    String getFileType();
    boolean isProcessed();
}

// TODO: Implement RealFileProcessor and FileProcessorProxy

Exercise 2: Intermediate Challenge (15 minutes)

Objective: Build a protection proxy with role-based access control

Requirements:

  • Create a DocumentService with operations: read, write, delete, share
  • Implement role-based permissions (READER, WRITER, ADMIN)
  • Create a ProtectedDocumentService proxy that enforces access control
  • Test different user roles and verify access restrictions

Hints:

  • Use enums for roles and operations
  • Consider using annotations or configuration for permission mapping
  • Implement audit logging for security events

Exercise 3: Creative Application (15 minutes)

Objective: Design a smart caching proxy with multiple cache strategies

Challenge: Create a proxy for a weather service that implements:

  • Time-based cache expiration
  • Geographic location-based caching
  • Fallback to cached data when service is unavailable
  • Cache warming and preloading for popular locations

Extension Ideas:

  • Implement cache statistics and monitoring
  • Add cache size limits with LRU eviction
  • Support cache invalidation patterns

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Proxy doesn’t properly delegate to real object Cause: Forgetting to call methods on the real subject Solution: Always ensure proxy methods delegate to real object when appropriate

  2. Problem: Circular dependencies or infinite recursion Cause: Proxy and real object referencing each other incorrectly Solution: Careful design of object relationships and initialization order

  3. Problem: Thread safety issues in cached proxies Cause: Multiple threads accessing and modifying cache concurrently Solution: Use concurrent collections or synchronization mechanisms

Debugging Tips

  • Use logging to trace proxy method calls and delegations
  • Verify that proxy and real object implement the same interface correctly
  • Test proxy behavior with null real objects and error conditions
  • Monitor cache hit rates and performance metrics

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Maintain interface compatibility - proxies should be drop-in replacements
  • Handle errors gracefully - proxy should not expose implementation details
  • Document proxy behavior - clearly specify what the proxy adds or changes
  • Consider thread safety especially for shared proxies and caches

Design Principles

  • Preserve original interface - don’t add proxy-specific methods to subject interface
  • Fail fast on configuration errors - validate proxy setup at creation time
  • Use dependency injection for configuring proxy chains and real objects
  • Monitor proxy performance - ensure proxy doesn’t become a bottleneck

Performance Considerations

  • Cache appropriately - balance memory usage against performance gains
  • Avoid proxy chains unless necessary - each proxy adds overhead
  • Profile proxy impact - measure actual performance improvement
  • Consider proxy lifecycle - when to create, cache, and clean up proxies

Next Steps in Your Learning Journey

  • Study AOP (Aspect-Oriented Programming) for advanced proxy techniques
  • Learn Decorator pattern for adding behavior vs controlling access
  • Explore Adapter pattern for interface compatibility vs access control
  • Investigate framework-specific proxy implementations (Spring AOP, Hibernate)

9. Summary and Review (5 minutes)

Key Takeaways

  1. Proxy controls access to another object while maintaining the same interface
  2. Different proxy types solve different problems - virtual, protection, remote, smart reference
  3. Proxies enable cross-cutting concerns without modifying original objects
  4. Choose appropriate proxy type based on your specific requirements (performance, security, etc.)

Quick Review Questions

  1. What’s the difference between virtual proxy and protection proxy?
  2. When would you use a proxy instead of directly modifying the original class?
  3. How do dynamic proxies differ from static proxies?

For Instructors

Teaching Tips

  • Start with concrete analogies (ATM, personal assistant) before technical details
  • Show performance measurements for virtual proxy effectiveness
  • Demonstrate security scenarios for protection proxy relevance
  • Use framework examples students might recognize (Hibernate, Spring)
  • Compare with Decorator pattern to clarify the difference

Common Student Questions

  • “When to use Proxy vs Decorator?” - Proxy controls access; Decorator adds behavior
  • “Are all proxies about performance?” - No, also security, remote access, lifecycle management
  • “How do frameworks use proxies?” - Show Spring AOP, ORM lazy loading examples

Assessment Ideas

  • Design challenge: Create proxy for specific domain problem (e.g., API rate limiting)
  • Performance analysis: Measure and compare proxy vs direct object access
  • Security review: Implement and test protection proxy with various attack scenarios

Adaptation Notes

  • Beginner focus: Emphasize the access control concept with simple examples
  • Advanced students: Discuss dynamic proxies, bytecode manipulation, AOP frameworks
  • Language-specific: Show language-specific proxy mechanisms (Python decorators, C# dynamic proxy)
  • Time management: The dynamic proxy section can be optional for shorter sessions