Proxy Pattern
Proxy Pattern: Controlling Object Access
Section titled “Proxy Pattern: Controlling Object Access”Now let’s explore the Proxy Pattern - a powerful structural design pattern that provides a placeholder or surrogate for another object to control access to it.
Why Proxy Pattern?
Section titled “Why Proxy Pattern?”Imagine you’re accessing a large file on a remote server. Instead of loading the entire file immediately (which is slow and expensive), you use a proxy that loads the file only when you actually need it. The Proxy Pattern works the same way - it provides a placeholder that controls access to the real object, adding functionality like lazy loading, access control, or caching.
The Proxy Pattern provides a surrogate or placeholder for another object to control access to it. The proxy has the same interface as the real object, allowing it to be used interchangeably.
What’s the Use of Proxy Pattern?
Section titled “What’s the Use of Proxy Pattern?”The Proxy Pattern is useful when:
- You want lazy loading - Load expensive objects only when needed
- You want access control - Control who can access the object
- You want caching - Cache results to avoid expensive operations
- You want logging/monitoring - Track access to objects
- You want remote access - Access objects on remote servers
What Happens If We Don’t Use Proxy Pattern?
Section titled “What Happens If We Don’t Use Proxy Pattern?”Without the Proxy Pattern, you might:
- Load everything upfront - Expensive objects loaded even if not used
- No access control - Direct access to sensitive objects
- No caching - Expensive operations repeated unnecessarily
- Tight coupling - Clients directly depend on real objects
- Hard to add cross-cutting concerns - Logging, security scattered everywhere
Simple Example: The Image Loader
Section titled “Simple Example: The Image Loader”Let’s start with a super simple example that anyone can understand!
Visual Representation
Section titled “Visual Representation”Interaction Flow
Section titled “Interaction Flow”Here’s how the Proxy Pattern works in practice - showing how proxy controls access:
The Problem
Section titled “The Problem”You’re building an image viewer. Loading images from disk is expensive. Without Proxy Pattern:
# ❌ Without Proxy Pattern - Loads everything upfront!
class RealImage: """Real image - expensive to load""" def __init__(self, filename: str): self.filename = filename self._load_from_disk() # Bad: Loads immediately!
def _load_from_disk(self): """Expensive operation - loads image from disk""" print(f"Loading {self.filename} from disk... (expensive operation)") # Simulate expensive loading import time time.sleep(1) # Simulate disk I/O
def display(self): """Display the image""" print(f"Displaying {self.filename}")
# Problem: All images loaded immediately, even if not displayed!def main(): # Bad: Loads all images upfront, even if not used! images = [ RealImage("photo1.jpg"), # Loaded immediately RealImage("photo2.jpg"), # Loaded immediately RealImage("photo3.jpg"), # Loaded immediately ]
# Only display first image, but all were loaded! images[0].display()
# Problems:# - All images loaded upfront (wasteful!)# - Slow startup time# - High memory usage# - No lazy loading// ❌ Without Proxy Pattern - Loads everything upfront!
public class RealImage { // Real image - expensive to load private String filename;
public RealImage(String filename) { this.filename = filename; loadFromDisk(); // Bad: Loads immediately! }
private void loadFromDisk() { // Expensive operation - loads image from disk System.out.println("Loading " + filename + " from disk... (expensive operation)"); // Simulate expensive loading try { Thread.sleep(1000); // Simulate disk I/O } catch (InterruptedException e) { e.printStackTrace(); } }
public void display() { // Display the image System.out.println("Displaying " + filename); }}
// Problem: All images loaded immediately, even if not displayed!public class Main { public static void main(String[] args) { // Bad: Loads all images upfront, even if not used! RealImage[] images = { new RealImage("photo1.jpg"), // Loaded immediately new RealImage("photo2.jpg"), // Loaded immediately new RealImage("photo3.jpg") // Loaded immediately };
// Only display first image, but all were loaded! images[0].display(); }}
// Problems:// - All images loaded upfront (wasteful!)// - Slow startup time// - High memory usage// - No lazy loading// ❌ Without Proxy Pattern - Loads everything upfront!
class RealImage { /** Real image - expensive to load */ private filename: string;
constructor(filename: string) { this.filename = filename; this.loadFromDisk(); // Bad: Loads immediately! }
private loadFromDisk(): void { /** Expensive operation - loads image from disk */ console.log(`Loading ${this.filename} from disk... (expensive operation)`); // Simulate expensive loading const start = Date.now(); while (Date.now() - start < 1000) {} // Simulate disk I/O }
display(): void { /** Display the image */ console.log(`Displaying ${this.filename}`); }}
// Problem: All images loaded immediately, even if not displayed!function main(): void { // Bad: Loads all images upfront, even if not used! const images = [ new RealImage("photo1.jpg"), // Loaded immediately new RealImage("photo2.jpg"), // Loaded immediately new RealImage("photo3.jpg"), // Loaded immediately ];
// Only display first image, but all were loaded! images[0].display();}
// Problems:// - All images loaded upfront (wasteful!)// - Slow startup time// - High memory usage// - No lazy loading// ❌ Without Proxy Pattern - Loads everything upfront!
#include <iostream>#include <string>#include <thread>#include <chrono>#include <vector>
class RealImage {private: std::string filename;
void loadFromDisk() { // Expensive operation - loads image from disk std::cout << "Loading " << filename << " from disk... (expensive operation)" << std::endl; // Simulate expensive loading std::this_thread::sleep_for(std::chrono::seconds(1)); // Simulate disk I/O }
public: RealImage(const std::string& filename) : filename(filename) { loadFromDisk(); // Bad: Loads immediately! }
void display() { // Display the image std::cout << "Displaying " << filename << std::endl; }};
// Problem: All images loaded immediately, even if not displayed!int main() { // Bad: Loads all images upfront, even if not used! std::vector<RealImage> images; images.emplace_back("photo1.jpg"); // Loaded immediately images.emplace_back("photo2.jpg"); // Loaded immediately images.emplace_back("photo3.jpg"); // Loaded immediately
// Only display first image, but all were loaded! images[0].display();
return 0;}
// Problems:// - All images loaded upfront (wasteful!)// - Slow startup time// - High memory usage// - No lazy loading// ❌ Without Proxy Pattern - Loads everything upfront!
using System;using System.Threading;
public class RealImage{ /** Real image - expensive to load */ private string filename;
public RealImage(string filename) { this.filename = filename; LoadFromDisk(); // Bad: Loads immediately! }
private void LoadFromDisk() { /** Expensive operation - loads image from disk */ Console.WriteLine($"Loading {filename} from disk... (expensive operation)"); // Simulate expensive loading Thread.Sleep(1000); // Simulate disk I/O }
public void Display() { /** Display the image */ Console.WriteLine($"Displaying {filename}"); }}
// Problem: All images loaded immediately, even if not displayed!class Program{ static void Main() { // Bad: Loads all images upfront, even if not used! RealImage[] images = { new RealImage("photo1.jpg"), // Loaded immediately new RealImage("photo2.jpg"), // Loaded immediately new RealImage("photo3.jpg") // Loaded immediately };
// Only display first image, but all were loaded! images[0].Display(); }}
// Problems:// - All images loaded upfront (wasteful!)// - Slow startup time// - High memory usage// - No lazy loading// ❌ Without Proxy Pattern - Loads everything upfront!
package main
import ( "fmt" "time")
type RealImageBad struct { filename string}
func NewRealImageBad(filename string) *RealImageBad { img := &RealImageBad{filename: filename} img.loadFromDisk() // Bad: Loads immediately! return img}
func (img *RealImageBad) loadFromDisk() { fmt.Printf("Loading %s from disk... (expensive operation)\n", img.filename) time.Sleep(10 * time.Millisecond) // Simulate disk I/O}
func (img *RealImageBad) Display() { fmt.Printf("Displaying %s\n", img.filename)}
func main() { // Bad: Loads all images upfront, even if not used! images := []*RealImageBad{ NewRealImageBad("photo1.jpg"), // Loaded immediately NewRealImageBad("photo2.jpg"), // Loaded immediately NewRealImageBad("photo3.jpg"), // Loaded immediately } // Only display first image, but all were loaded! images[0].Display()}
// Problems:// - All images loaded upfront (wasteful!)// - Slow startup time// - High memory usage// - No lazy loading// The Problemstruct Image { filename: String,}impl Image { fn new(filename: &str) -> Self { println!("Loading {}", filename); Self { filename: filename.into(), } } fn display(&self) { println!("Display {}", self.filename); }}Problems:
- Loads everything upfront - All images loaded even if not used
- Slow startup - Expensive operations happen immediately
- High memory usage - All objects in memory at once
- No lazy loading - Can’t defer expensive operations
The Solution: Proxy Pattern
Section titled “The Solution: Proxy Pattern”Class Structure
Section titled “Class Structure”from abc import ABC, abstractmethod
# Step 1: Define the Subject interfaceclass Image(ABC): """Subject interface - what images should do"""
@abstractmethod def display(self) -> None: """Display the image""" pass
# Step 2: Implement Real Subjectclass RealImage(Image): """Real image - expensive to load"""
def __init__(self, filename: str): self.filename = filename self._load_from_disk()
def _load_from_disk(self): """Expensive operation - loads image from disk""" print(f"Loading {self.filename} from disk... (expensive operation)") # Simulate expensive loading import time time.sleep(1) # Simulate disk I/O
def display(self): """Display the image""" print(f"Displaying {self.filename}")
# Step 3: Create Proxyclass ImageProxy(Image): """Proxy - controls access to real image"""
def __init__(self, filename: str): self.filename = filename self._real_image: RealImage = None # Lazy loading - not loaded yet!
def display(self): """Display image - loads only when needed""" if self._real_image is None: # Lazy loading - load only when actually needed! print(f"Proxy: Loading {self.filename} on demand...") self._real_image = RealImage(self.filename)
# Delegate to real image self._real_image.display()
# Usage - Lazy loading!def main(): print("Creating image proxies (no loading yet)...\n")
# Good: Only creates proxies, doesn't load images! images = [ ImageProxy("photo1.jpg"), # Proxy created, image not loaded ImageProxy("photo2.jpg"), # Proxy created, image not loaded ImageProxy("photo3.jpg"), # Proxy created, image not loaded ]
print("Proxies created. No images loaded yet!\n")
# Only load when actually needed! print("Displaying first image (loads on demand):") images[0].display()
print("\nDisplaying first image again (uses cached):") images[0].display() # Uses cached real image
print("\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!")
if __name__ == "__main__": main()// Step 1: Define the Subject interfaceinterface Image { // Subject interface - what images should do void display();}
// Step 2: Implement Real Subjectclass RealImage implements Image { // Real image - expensive to load private String filename;
public RealImage(String filename) { this.filename = filename; loadFromDisk(); }
private void loadFromDisk() { // Expensive operation - loads image from disk System.out.println("Loading " + filename + " from disk... (expensive operation)"); // Simulate expensive loading try { Thread.sleep(1000); // Simulate disk I/O } catch (InterruptedException e) { e.printStackTrace(); } }
@Override public void display() { // Display the image System.out.println("Displaying " + filename); }}
// Step 3: Create Proxyclass ImageProxy implements Image { // Proxy - controls access to real image private String filename; private RealImage realImage; // Lazy loading - not loaded yet!
public ImageProxy(String filename) { this.filename = filename; }
@Override public void display() { // Display image - loads only when needed if (realImage == null) { // Lazy loading - load only when actually needed! System.out.println("Proxy: Loading " + filename + " on demand..."); realImage = new RealImage(filename); }
// Delegate to real image realImage.display(); }}
// Usage - Lazy loading!public class Main { public static void main(String[] args) { System.out.println("Creating image proxies (no loading yet)...\n");
// Good: Only creates proxies, doesn't load images! Image[] images = { new ImageProxy("photo1.jpg"), // Proxy created, image not loaded new ImageProxy("photo2.jpg"), // Proxy created, image not loaded new ImageProxy("photo3.jpg") // Proxy created, image not loaded };
System.out.println("Proxies created. No images loaded yet!\n");
// Only load when actually needed! System.out.println("Displaying first image (loads on demand):"); images[0].display();
System.out.println("\nDisplaying first image again (uses cached):"); images[0].display(); // Uses cached real image
System.out.println("\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!"); }}// Step 1: Define the Subject interfaceinterface Image { /** Subject interface - what images should do */ display(): void;}
// Step 2: Implement Real Subjectclass RealImage implements Image { /** Real image - expensive to load */ private filename: string;
constructor(filename: string) { this.filename = filename; this.loadFromDisk(); }
private loadFromDisk(): void { /** Expensive operation - loads image from disk */ console.log(`Loading ${this.filename} from disk... (expensive operation)`); // Simulate expensive loading const start = Date.now(); while (Date.now() - start < 1000) {} // Simulate disk I/O }
display(): void { /** Display the image */ console.log(`Displaying ${this.filename}`); }}
// Step 3: Create Proxyclass ImageProxy implements Image { /** Proxy - controls access to real image */ private filename: string; private realImage: RealImage | null = null; // Lazy loading - not loaded yet!
constructor(filename: string) { this.filename = filename; }
display(): void { /** Display image - loads only when needed */ if (this.realImage === null) { // Lazy loading - load only when actually needed! console.log(`Proxy: Loading ${this.filename} on demand...`); this.realImage = new RealImage(this.filename); }
// Delegate to real image this.realImage.display(); }}
// Usage - Lazy loading!function main(): void { console.log("Creating image proxies (no loading yet)...\n");
// Good: Only creates proxies, doesn't load images! const images: Image[] = [ new ImageProxy("photo1.jpg"), // Proxy created, image not loaded new ImageProxy("photo2.jpg"), // Proxy created, image not loaded new ImageProxy("photo3.jpg"), // Proxy created, image not loaded ];
console.log("Proxies created. No images loaded yet!\n");
// Only load when actually needed! console.log("Displaying first image (loads on demand):"); images[0].display();
console.log("\nDisplaying first image again (uses cached):"); images[0].display(); // Uses cached real image
console.log("\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!");}
main();#include <iostream>#include <string>#include <thread>#include <chrono>#include <memory>#include <vector>
// Step 1: Define the Subject interfaceclass Image {public: virtual ~Image() = default; // Subject interface - what images should do virtual void display() = 0;};
// Step 2: Implement Real Subjectclass RealImage : public Image {private: std::string filename;
void loadFromDisk() { // Expensive operation - loads image from disk std::cout << "Loading " << filename << " from disk... (expensive operation)" << std::endl; // Simulate expensive loading std::this_thread::sleep_for(std::chrono::seconds(1)); // Simulate disk I/O }
public: RealImage(const std::string& filename) : filename(filename) { loadFromDisk(); }
void display() override { // Display the image std::cout << "Displaying " << filename << std::endl; }};
// Step 3: Create Proxyclass ImageProxy : public Image {private: std::string filename; std::unique_ptr<RealImage> realImage; // Lazy loading - not loaded yet!
public: ImageProxy(const std::string& filename) : filename(filename), realImage(nullptr) {}
void display() override { // Display image - loads only when needed if (realImage == nullptr) { // Lazy loading - load only when actually needed! std::cout << "Proxy: Loading " << filename << " on demand..." << std::endl; realImage = std::make_unique<RealImage>(filename); }
// Delegate to real image realImage->display(); }};
// Usage - Lazy loading!int main() { std::cout << "Creating image proxies (no loading yet)...\n" << std::endl;
// Good: Only creates proxies, doesn't load images! std::vector<std::unique_ptr<Image>> images; images.push_back(std::make_unique<ImageProxy>("photo1.jpg")); // Proxy created, image not loaded images.push_back(std::make_unique<ImageProxy>("photo2.jpg")); // Proxy created, image not loaded images.push_back(std::make_unique<ImageProxy>("photo3.jpg")); // Proxy created, image not loaded
std::cout << "Proxies created. No images loaded yet!\n" << std::endl;
// Only load when actually needed! std::cout << "Displaying first image (loads on demand):" << std::endl; images[0]->display();
std::cout << "\nDisplaying first image again (uses cached):" << std::endl; images[0]->display(); // Uses cached real image
std::cout << "\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!" << std::endl;
return 0;}using System;using System.Threading;
// Step 1: Define the Subject interfacepublic interface IImage{ /** Subject interface - what images should do */ void Display();}
// Step 2: Implement Real Subjectpublic class RealImage : IImage{ /** Real image - expensive to load */ private string filename;
public RealImage(string filename) { this.filename = filename; LoadFromDisk(); }
private void LoadFromDisk() { /** Expensive operation - loads image from disk */ Console.WriteLine($"Loading {filename} from disk... (expensive operation)"); // Simulate expensive loading Thread.Sleep(1000); // Simulate disk I/O }
public void Display() { /** Display the image */ Console.WriteLine($"Displaying {filename}"); }}
// Step 3: Create Proxypublic class ImageProxy : IImage{ /** Proxy - controls access to real image */ private string filename; private RealImage realImage = null; // Lazy loading - not loaded yet!
public ImageProxy(string filename) { this.filename = filename; }
public void Display() { /** Display image - loads only when needed */ if (realImage == null) { // Lazy loading - load only when actually needed! Console.WriteLine($"Proxy: Loading {filename} on demand..."); realImage = new RealImage(filename); }
// Delegate to real image realImage.Display(); }}
// Usage - Lazy loading!class Program{ static void Main() { Console.WriteLine("Creating image proxies (no loading yet)...\n");
// Good: Only creates proxies, doesn't load images! IImage[] images = { new ImageProxy("photo1.jpg"), // Proxy created, image not loaded new ImageProxy("photo2.jpg"), // Proxy created, image not loaded new ImageProxy("photo3.jpg") // Proxy created, image not loaded };
Console.WriteLine("Proxies created. No images loaded yet!\n");
// Only load when actually needed! Console.WriteLine("Displaying first image (loads on demand):"); images[0].Display();
Console.WriteLine("\nDisplaying first image again (uses cached):"); images[0].Display(); // Uses cached real image
Console.WriteLine("\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!"); }}package main
import ( "fmt" "time")
// Step 1: Subject interfacetype Image interface { Display()}
// Step 2: Real Subjecttype RealImage struct { filename string}
func NewRealImage(filename string) *RealImage { img := &RealImage{filename: filename} img.loadFromDisk() return img}
func (img *RealImage) loadFromDisk() { fmt.Printf("Loading %s from disk... (expensive operation)\n", img.filename) time.Sleep(10 * time.Millisecond)}
func (img *RealImage) Display() { fmt.Printf("Displaying %s\n", img.filename)}
// Step 3: Proxytype ImageProxy struct { filename string realImage *RealImage // nil until needed}
func NewImageProxy(filename string) *ImageProxy { return &ImageProxy{filename: filename}}
func (p *ImageProxy) Display() { if p.realImage == nil { // Lazy loading - load only when actually needed! fmt.Printf("Proxy: Loading %s on demand...\n", p.filename) p.realImage = NewRealImage(p.filename) } p.realImage.Display()}
func main() { fmt.Println("Creating image proxies (no loading yet)...\n")
images := []Image{ NewImageProxy("photo1.jpg"), NewImageProxy("photo2.jpg"), NewImageProxy("photo3.jpg"), }
fmt.Println("Proxies created. No images loaded yet!\n")
fmt.Println("Displaying first image (loads on demand):") images[0].Display()
fmt.Println("\nDisplaying first image again (uses cached):") images[0].Display() // Uses cached real image
fmt.Println("\n✅ Proxy Pattern: Images loaded only when needed (lazy loading)!")}// Class Structuretrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}Real-World Software Example: Database Query Proxy
Section titled “Real-World Software Example: Database Query Proxy”Now let’s see a realistic software example - a database system that needs to cache query results to avoid expensive database calls.
The Problem
Section titled “The Problem”You’re building a database access layer. Database queries are expensive. Without Proxy Pattern:
# ❌ Without Proxy Pattern - No caching!
class Database: """Real database - expensive queries"""
def execute_query(self, query: str) -> dict: """Expensive database query""" print(f"Executing query: {query} (expensive operation)") # Simulate expensive database operation import time time.sleep(2) # Simulate database I/O
# Return mock result return {"result": f"Data for {query}", "rows": 100}
def get_user(self, user_id: int) -> dict: """Get user by ID""" return self.execute_query(f"SELECT * FROM users WHERE id = {user_id}")
# Problem: Every query hits the database, even if same query!def main(): db = Database()
# Same query executed multiple times - wasteful! result1 = db.get_user(1) # Hits database result2 = db.get_user(1) # Hits database again (same query!) result3 = db.get_user(1) # Hits database again (same query!)
print("❌ No caching - same query executed 3 times!")
# Problems:# - No caching - same queries executed repeatedly# - Slow performance# - Unnecessary database load// ❌ Without Proxy Pattern - No caching!
public class Database { // Real database - expensive queries public Map<String, Object> executeQuery(String query) { // Expensive database query System.out.println("Executing query: " + query + " (expensive operation)"); // Simulate expensive database operation try { Thread.sleep(2000); // Simulate database I/O } catch (InterruptedException e) { e.printStackTrace(); }
// Return mock result Map<String, Object> result = new HashMap<>(); result.put("result", "Data for " + query); result.put("rows", 100); return result; }
public Map<String, Object> getUser(int userId) { return executeQuery("SELECT * FROM users WHERE id = " + userId); }}
// Problem: Every query hits the database, even if same query!public class Main { public static void main(String[] args) { Database db = new Database();
// Same query executed multiple times - wasteful! db.getUser(1); // Hits database db.getUser(1); // Hits database again (same query!) db.getUser(1); // Hits database again (same query!)
System.out.println("❌ No caching - same query executed 3 times!"); }}
// Problems:// - No caching - same queries executed repeatedly// - Slow performance// - Unnecessary database load// ❌ Without Proxy Pattern - No caching!
class Database { /** Real database - expensive queries */ executeQuery(query: string): Record<string, any> { /** Expensive database query */ console.log(`Executing query: ${query} (expensive operation)`); // Simulate expensive database operation const start = Date.now(); while (Date.now() - start < 2000) {} // Simulate database I/O
// Return mock result return { result: `Data for ${query}`, rows: 100 }; }
getUser(userId: number): Record<string, any> { return this.executeQuery(`SELECT * FROM users WHERE id = ${userId}`); }}
// Problem: Every query hits the database, even if same query!function main(): void { const db = new Database();
// Same query executed multiple times - wasteful! db.getUser(1); // Hits database db.getUser(1); // Hits database again (same query!) db.getUser(1); // Hits database again (same query!)
console.log("❌ No caching - same query executed 3 times!");}
// Problems:// - No caching - same queries executed repeatedly// - Slow performance// - Unnecessary database load// ❌ Without Proxy Pattern - No caching!
#include <iostream>#include <string>#include <map>#include <thread>#include <chrono>
class Database {public: // Real database - expensive queries std::map<std::string, std::string> executeQuery(const std::string& query) { // Expensive database query std::cout << "Executing query: " << query << " (expensive operation)" << std::endl; // Simulate expensive database operation std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate database I/O
// Return mock result std::map<std::string, std::string> result; result["result"] = "Data for " + query; result["rows"] = "100"; return result; }
std::map<std::string, std::string> getUser(int userId) { return executeQuery("SELECT * FROM users WHERE id = " + std::to_string(userId)); }};
// Problem: Every query hits the database, even if same query!int main() { Database db;
// Same query executed multiple times - wasteful! db.getUser(1); // Hits database db.getUser(1); // Hits database again (same query!) db.getUser(1); // Hits database again (same query!)
std::cout << "❌ No caching - same query executed 3 times!" << std::endl;
return 0;}
// Problems:// - No caching - same queries executed repeatedly// - Slow performance// - Unnecessary database load// ❌ Without Proxy Pattern - No caching!
using System;using System.Collections.Generic;using System.Threading;
public class Database{ /** Real database - expensive queries */ public Dictionary<string, object> ExecuteQuery(string query) { /** Expensive database query */ Console.WriteLine($"Executing query: {query} (expensive operation)"); // Simulate expensive database operation Thread.Sleep(2000); // Simulate database I/O
// Return mock result return new Dictionary<string, object> { ["result"] = $"Data for {query}", ["rows"] = 100 }; }
public Dictionary<string, object> GetUser(int userId) { return ExecuteQuery($"SELECT * FROM users WHERE id = {userId}"); }}
// Problem: Every query hits the database, even if same query!class Program{ static void Main() { Database db = new Database();
// Same query executed multiple times - wasteful! db.GetUser(1); // Hits database db.GetUser(1); // Hits database again (same query!) db.GetUser(1); // Hits database again (same query!)
Console.WriteLine("❌ No caching - same query executed 3 times!"); }}
// Problems:// - No caching - same queries executed repeatedly// - Slow performance// - Unnecessary database load// ❌ Without Proxy Pattern - No caching!
package main
import ( "fmt" "time")
type DatabaseBad struct{}
func (db *DatabaseBad) ExecuteQuery(query string) map[string]interface{} { fmt.Printf("Executing query: %s (expensive operation)\n", query) time.Sleep(20 * time.Millisecond) // Simulate database I/O return map[string]interface{}{"result": "Data for " + query, "rows": 100}}
func (db *DatabaseBad) GetUser(userID int) map[string]interface{} { return db.ExecuteQuery(fmt.Sprintf("SELECT * FROM users WHERE id = %d", userID))}
func main() { db := &DatabaseBad{} db.GetUser(1) // Hits database db.GetUser(1) // Hits database again (same query!) db.GetUser(1) // Hits database again (same query!) fmt.Println("No caching - same query executed 3 times!")}
// Problems:// - No caching - same queries executed repeatedly// - Slow performance// - Unnecessary database load// The Problemstruct Image { filename: String,}impl Image { fn new(filename: &str) -> Self { println!("Loading {}", filename); Self { filename: filename.into(), } } fn display(&self) { println!("Display {}", self.filename); }}Problems:
- No caching - Same queries executed repeatedly
- Slow performance - Every query hits the database
- Unnecessary load - Database accessed even for cached queries
The Solution: Proxy Pattern
Section titled “The Solution: Proxy Pattern”from abc import ABC, abstractmethodfrom typing import Dict, Anyimport hashlibimport json
# Step 1: Define the Subject interfaceclass DatabaseInterface(ABC): """Subject interface - database operations"""
@abstractmethod def get_user(self, user_id: int) -> Dict[str, Any]: """Get user by ID""" pass
# Step 2: Implement Real Subjectclass Database(DatabaseInterface): """Real database - expensive queries"""
def execute_query(self, query: str) -> Dict[str, Any]: """Expensive database query""" print(f"Database: Executing query: {query} (expensive operation)") # Simulate expensive database operation import time time.sleep(2) # Simulate database I/O
# Return mock result return {"result": f"Data for {query}", "rows": 100}
def get_user(self, user_id: int) -> Dict[str, Any]: """Get user by ID""" return self.execute_query(f"SELECT * FROM users WHERE id = {user_id}")
# Step 3: Create Proxy with Cachingclass DatabaseProxy(DatabaseInterface): """Proxy - adds caching to database access"""
def __init__(self, database: Database): self._database = database self._cache: Dict[str, Dict[str, Any]] = {} # Cache for query results
def _get_cache_key(self, method: str, *args) -> str: """Generate cache key from method and arguments""" key_data = {"method": method, "args": args} key_string = json.dumps(key_data, sort_keys=True) return hashlib.md5(key_string.encode()).hexdigest()
def get_user(self, user_id: int) -> Dict[str, Any]: """Get user by ID - with caching""" cache_key = self._get_cache_key("get_user", user_id)
# Check cache first if cache_key in self._cache: print(f"Proxy: Cache hit for user {user_id} (fast!)") return self._cache[cache_key]
# Cache miss - query database print(f"Proxy: Cache miss for user {user_id}, querying database...") result = self._database.get_user(user_id)
# Store in cache self._cache[cache_key] = result print(f"Proxy: Cached result for user {user_id}")
return result
def clear_cache(self): """Clear the cache""" self._cache.clear() print("Proxy: Cache cleared")
# Usage - With caching!def main(): database = Database() proxy = DatabaseProxy(database)
print("=== First call (cache miss) ===\n") result1 = proxy.get_user(1) # Cache miss - queries database
print("\n=== Second call (cache hit) ===\n") result2 = proxy.get_user(1) # Cache hit - returns cached result
print("\n=== Third call (cache hit) ===\n") result3 = proxy.get_user(1) # Cache hit - returns cached result
print("\n✅ Proxy Pattern: Caching reduces database load!")
if __name__ == "__main__": main()import java.util.*;import java.security.MessageDigest;
// Step 1: Define the Subject interfaceinterface DatabaseInterface { // Subject interface - database operations Map<String, Object> getUser(int userId);}
// Step 2: Implement Real Subjectclass Database implements DatabaseInterface { // Real database - expensive queries public Map<String, Object> executeQuery(String query) { // Expensive database query System.out.println("Database: Executing query: " + query + " (expensive operation)"); // Simulate expensive database operation try { Thread.sleep(2000); // Simulate database I/O } catch (InterruptedException e) { e.printStackTrace(); }
// Return mock result Map<String, Object> result = new HashMap<>(); result.put("result", "Data for " + query); result.put("rows", 100); return result; }
@Override public Map<String, Object> getUser(int userId) { return executeQuery("SELECT * FROM users WHERE id = " + userId); }}
// Step 3: Create Proxy with Cachingclass DatabaseProxy implements DatabaseInterface { // Proxy - adds caching to database access private Database database; private Map<String, Map<String, Object>> cache; // Cache for query results
public DatabaseProxy(Database database) { this.database = database; this.cache = new HashMap<>(); }
private String getCacheKey(String method, Object... args) { // Generate cache key from method and arguments StringBuilder keyBuilder = new StringBuilder(method); for (Object arg : args) { keyBuilder.append("_").append(arg); } return keyBuilder.toString(); }
@Override public Map<String, Object> getUser(int userId) { // Get user by ID - with caching String cacheKey = getCacheKey("getUser", userId);
// Check cache first if (cache.containsKey(cacheKey)) { System.out.println("Proxy: Cache hit for user " + userId + " (fast!)"); return cache.get(cacheKey); }
// Cache miss - query database System.out.println("Proxy: Cache miss for user " + userId + ", querying database..."); Map<String, Object> result = database.getUser(userId);
// Store in cache cache.put(cacheKey, result); System.out.println("Proxy: Cached result for user " + userId);
return result; }
public void clearCache() { // Clear the cache cache.clear(); System.out.println("Proxy: Cache cleared"); }}
// Usage - With caching!public class Main { public static void main(String[] args) { Database database = new Database(); DatabaseProxy proxy = new DatabaseProxy(database);
System.out.println("=== First call (cache miss) ===\n"); proxy.getUser(1); // Cache miss - queries database
System.out.println("\n=== Second call (cache hit) ===\n"); proxy.getUser(1); // Cache hit - returns cached result
System.out.println("\n=== Third call (cache hit) ===\n"); proxy.getUser(1); // Cache hit - returns cached result
System.out.println("\n✅ Proxy Pattern: Caching reduces database load!"); }}import * as crypto from 'crypto';
// Step 1: Define the Subject interfaceinterface DatabaseInterface { /** Subject interface - database operations */ getUser(userId: number): Record<string, any>;}
// Step 2: Implement Real Subjectclass Database implements DatabaseInterface { /** Real database - expensive queries */ executeQuery(query: string): Record<string, any> { /** Expensive database query */ console.log(`Database: Executing query: ${query} (expensive operation)`); // Simulate expensive database operation const start = Date.now(); while (Date.now() - start < 2000) {} // Simulate database I/O
// Return mock result return { result: `Data for ${query}`, rows: 100 }; }
getUser(userId: number): Record<string, any> { /** Get user by ID */ return this.executeQuery(`SELECT * FROM users WHERE id = ${userId}`); }}
// Step 3: Create Proxy with Cachingclass DatabaseProxy implements DatabaseInterface { /** Proxy - adds caching to database access */ private database: Database; private cache: Map<string, Record<string, any>> = new Map();
constructor(database: Database) { this.database = database; }
private getCacheKey(method: string, ...args: any[]): string { /** Generate cache key from method and arguments */ const keyData = JSON.stringify({ method, args }); return crypto.createHash('md5').update(keyData).digest('hex'); }
getUser(userId: number): Record<string, any> { /** Get user by ID - with caching */ const cacheKey = this.getCacheKey("getUser", userId);
// Check cache first if (this.cache.has(cacheKey)) { console.log(`Proxy: Cache hit for user ${userId} (fast!)`); return this.cache.get(cacheKey)!; }
// Cache miss - query database console.log(`Proxy: Cache miss for user ${userId}, querying database...`); const result = this.database.getUser(userId);
// Store in cache this.cache.set(cacheKey, result); console.log(`Proxy: Cached result for user ${userId}`);
return result; }
clearCache(): void { /** Clear the cache */ this.cache.clear(); console.log("Proxy: Cache cleared"); }}
// Usage - With caching!function main(): void { const database = new Database(); const proxy = new DatabaseProxy(database);
console.log("=== First call (cache miss) ===\n"); proxy.getUser(1); // Cache miss - queries database
console.log("\n=== Second call (cache hit) ===\n"); proxy.getUser(1); // Cache hit - returns cached result
console.log("\n=== Third call (cache hit) ===\n"); proxy.getUser(1); // Cache hit - returns cached result
console.log("\n✅ Proxy Pattern: Caching reduces database load!");}
main();#include <iostream>#include <string>#include <map>#include <thread>#include <chrono>#include <sstream>
// Step 1: Define the Subject interfaceclass DatabaseInterface {public: virtual ~DatabaseInterface() = default; // Subject interface - database operations virtual std::map<std::string, std::string> getUser(int userId) = 0;};
// Step 2: Implement Real Subjectclass Database : public DatabaseInterface {public: // Real database - expensive queries std::map<std::string, std::string> executeQuery(const std::string& query) { // Expensive database query std::cout << "Database: Executing query: " << query << " (expensive operation)" << std::endl; // Simulate expensive database operation std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate database I/O
// Return mock result std::map<std::string, std::string> result; result["result"] = "Data for " + query; result["rows"] = "100"; return result; }
std::map<std::string, std::string> getUser(int userId) override { // Get user by ID return executeQuery("SELECT * FROM users WHERE id = " + std::to_string(userId)); }};
// Step 3: Create Proxy with Cachingclass DatabaseProxy : public DatabaseInterface {private: Database* database; std::map<std::string, std::map<std::string, std::string>> cache;
std::string getCacheKey(const std::string& method, int arg) { // Generate cache key from method and arguments std::ostringstream oss; oss << method << "_" << arg; return oss.str(); }
public: DatabaseProxy(Database* database) : database(database) {}
std::map<std::string, std::string> getUser(int userId) override { // Get user by ID - with caching std::string cacheKey = getCacheKey("getUser", userId);
// Check cache first if (cache.find(cacheKey) != cache.end()) { std::cout << "Proxy: Cache hit for user " << userId << " (fast!)" << std::endl; return cache[cacheKey]; }
// Cache miss - query database std::cout << "Proxy: Cache miss for user " << userId << ", querying database..." << std::endl; std::map<std::string, std::string> result = database->getUser(userId);
// Store in cache cache[cacheKey] = result; std::cout << "Proxy: Cached result for user " << userId << std::endl;
return result; }
void clearCache() { // Clear the cache cache.clear(); std::cout << "Proxy: Cache cleared" << std::endl; }};
// Usage - With caching!int main() { Database database; DatabaseProxy proxy(&database);
std::cout << "=== First call (cache miss) ===\n" << std::endl; proxy.getUser(1); // Cache miss - queries database
std::cout << "\n=== Second call (cache hit) ===\n" << std::endl; proxy.getUser(1); // Cache hit - returns cached result
std::cout << "\n=== Third call (cache hit) ===\n" << std::endl; proxy.getUser(1); // Cache hit - returns cached result
std::cout << "\n✅ Proxy Pattern: Caching reduces database load!" << std::endl;
return 0;}using System;using System.Collections.Generic;using System.Threading;using System.Security.Cryptography;using System.Text;
// Step 1: Define the Subject interfacepublic interface IDatabaseInterface{ /** Subject interface - database operations */ Dictionary<string, object> GetUser(int userId);}
// Step 2: Implement Real Subjectpublic class Database : IDatabaseInterface{ /** Real database - expensive queries */ public Dictionary<string, object> ExecuteQuery(string query) { /** Expensive database query */ Console.WriteLine($"Database: Executing query: {query} (expensive operation)"); // Simulate expensive database operation Thread.Sleep(2000); // Simulate database I/O
// Return mock result return new Dictionary<string, object> { ["result"] = $"Data for {query}", ["rows"] = 100 }; }
public Dictionary<string, object> GetUser(int userId) { /** Get user by ID */ return ExecuteQuery($"SELECT * FROM users WHERE id = {userId}"); }}
// Step 3: Create Proxy with Cachingpublic class DatabaseProxy : IDatabaseInterface{ /** Proxy - adds caching to database access */ private Database database; private Dictionary<string, Dictionary<string, object>> cache = new Dictionary<string, Dictionary<string, object>>();
public DatabaseProxy(Database database) { this.database = database; }
private string GetCacheKey(string method, params object[] args) { /** Generate cache key from method and arguments */ string keyData = $"{method}_{string.Join("_", args)}"; using (MD5 md5 = MD5.Create()) { byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(keyData)); return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } }
public Dictionary<string, object> GetUser(int userId) { /** Get user by ID - with caching */ string cacheKey = GetCacheKey("GetUser", userId);
// Check cache first if (cache.ContainsKey(cacheKey)) { Console.WriteLine($"Proxy: Cache hit for user {userId} (fast!)"); return cache[cacheKey]; }
// Cache miss - query database Console.WriteLine($"Proxy: Cache miss for user {userId}, querying database..."); Dictionary<string, object> result = database.GetUser(userId);
// Store in cache cache[cacheKey] = result; Console.WriteLine($"Proxy: Cached result for user {userId}");
return result; }
public void ClearCache() { /** Clear the cache */ cache.Clear(); Console.WriteLine("Proxy: Cache cleared"); }}
// Usage - With caching!class Program{ static void Main() { Database database = new Database(); DatabaseProxy proxy = new DatabaseProxy(database);
Console.WriteLine("=== First call (cache miss) ===\n"); proxy.GetUser(1); // Cache miss - queries database
Console.WriteLine("\n=== Second call (cache hit) ===\n"); proxy.GetUser(1); // Cache hit - returns cached result
Console.WriteLine("\n=== Third call (cache hit) ===\n"); proxy.GetUser(1); // Cache hit - returns cached result
Console.WriteLine("\n✅ Proxy Pattern: Caching reduces database load!"); }}package main
import ( "fmt" "time")
// Step 1: Subject interfacetype DatabaseInterface interface { GetUser(userID int) map[string]interface{}}
// Step 2: Real Subjecttype Database struct{}
func (db *Database) ExecuteQuery(query string) map[string]interface{} { fmt.Printf("Database: Executing query: %s (expensive operation)\n", query) time.Sleep(20 * time.Millisecond) return map[string]interface{}{"result": "Data for " + query, "rows": 100}}
func (db *Database) GetUser(userID int) map[string]interface{} { return db.ExecuteQuery(fmt.Sprintf("SELECT * FROM users WHERE id = %d", userID))}
// Step 3: Proxy with Cachingtype DatabaseProxy struct { database *Database cache map[string]map[string]interface{}}
func NewDatabaseProxy(db *Database) *DatabaseProxy { return &DatabaseProxy{database: db, cache: make(map[string]map[string]interface{})}}
func (p *DatabaseProxy) GetUser(userID int) map[string]interface{} { key := fmt.Sprintf("GetUser_%d", userID) if cached, ok := p.cache[key]; ok { fmt.Printf("Proxy: Cache hit for user %d (fast!)\n", userID) return cached } fmt.Printf("Proxy: Cache miss for user %d, querying database...\n", userID) result := p.database.GetUser(userID) p.cache[key] = result fmt.Printf("Proxy: Cached result for user %d\n", userID) return result}
func main() { db := &Database{} proxy := NewDatabaseProxy(db)
fmt.Println("=== First call (cache miss) ===\n") proxy.GetUser(1)
fmt.Println("\n=== Second call (cache hit) ===\n") proxy.GetUser(1)
fmt.Println("\n=== Third call (cache hit) ===\n") proxy.GetUser(1)
fmt.Println("\n✅ Proxy Pattern: Caching reduces database load!")}// The Solution: Proxy Patterntrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}Proxy Pattern Variants
Section titled “Proxy Pattern Variants”There are different types of proxies based on their purpose:
1. Virtual Proxy (Lazy Loading)
Section titled “1. Virtual Proxy (Lazy Loading)”Loads expensive objects only when needed:
# Virtual Proxy - lazy loadingclass Proxy: def __init__(self): self._real_object = None # Not loaded yet
def operation(self): if self._real_object is None: self._real_object = ExpensiveObject() # Load on demand return self._real_object.operation()// Virtual Proxy - lazy loadingclass Proxy implements Subject { private RealSubject realSubject; // Not loaded yet
@Override public void operation() { if (realSubject == null) { realSubject = new RealSubject(); // Load on demand } realSubject.operation(); }}// Virtual Proxy - lazy loadinginterface Subject { operation(): void;}
class RealSubject implements Subject { operation(): void { // Expensive operation }}
class Proxy implements Subject { private realSubject: RealSubject | null = null; // Not loaded yet
operation(): void { if (this.realSubject === null) { this.realSubject = new RealSubject(); // Load on demand } this.realSubject.operation(); }}// Virtual Proxy - lazy loadingclass Subject {public: virtual ~Subject() = default; virtual void operation() = 0;};
class RealSubject : public Subject {public: void operation() override { // Expensive operation }};
class Proxy : public Subject {private: RealSubject* realSubject = nullptr; // Not loaded yet
public: ~Proxy() { delete realSubject; }
void operation() override { if (realSubject == nullptr) { realSubject = new RealSubject(); // Load on demand } realSubject->operation(); }};// Virtual Proxy - lazy loadingpublic interface ISubject{ void Operation();}
public class RealSubject : ISubject{ public void Operation() { // Expensive operation }}
public class Proxy : ISubject{ private RealSubject realSubject = null; // Not loaded yet
public void Operation() { if (realSubject == null) { realSubject = new RealSubject(); // Load on demand } realSubject.Operation(); }}package main
// Virtual Proxy - lazy loadingtype VSubject interface { Operation()}
type VRealSubject struct{}func (r *VRealSubject) Operation() { /* Expensive operation */ }
type VProxy struct { realSubject *VRealSubject // nil until needed}
func (p *VProxy) Operation() { if p.realSubject == nil { p.realSubject = &VRealSubject{} // Load on demand } p.realSubject.Operation()}// 1. Virtual Proxy Lazy Loadingtrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}Use case: Lazy loading of expensive resources
2. Protection Proxy (Access Control)
Section titled “2. Protection Proxy (Access Control)”Controls access to objects based on permissions:
# Protection Proxy - access controlclass ProtectionProxy: def __init__(self, user_role: str): self._real_object = RealObject() self._user_role = user_role
def operation(self): if self._user_role != "admin": raise PermissionError("Access denied") return self._real_object.operation()// Protection Proxy - access controlclass ProtectionProxy implements Subject { private RealSubject realSubject; private String userRole;
public ProtectionProxy(String userRole) { this.realSubject = new RealSubject(); this.userRole = userRole; }
@Override public void operation() { if (!"admin".equals(userRole)) { throw new SecurityException("Access denied"); } realSubject.operation(); }}// Protection Proxy - access controlinterface Subject { operation(): void;}
class RealObject implements Subject { operation(): void { // Real operation }}
class ProtectionProxy implements Subject { private realObject: RealObject; private userRole: string;
constructor(userRole: string) { this.realObject = new RealObject(); this.userRole = userRole; }
operation(): void { if (this.userRole !== "admin") { throw new Error("Access denied"); } this.realObject.operation(); }}#include <stdexcept>#include <string>
// Protection Proxy - access controlclass Subject {public: virtual ~Subject() = default; virtual void operation() = 0;};
class RealSubject : public Subject {public: void operation() override { // Real operation }};
class ProtectionProxy : public Subject {private: RealSubject* realSubject; std::string userRole;
public: ProtectionProxy(const std::string& userRole) : userRole(userRole) { realSubject = new RealSubject(); }
~ProtectionProxy() { delete realSubject; }
void operation() override { if (userRole != "admin") { throw std::runtime_error("Access denied"); } realSubject->operation(); }};using System;
// Protection Proxy - access controlpublic interface ISubject{ void Operation();}
public class RealObject : ISubject{ public void Operation() { // Real operation }}
public class ProtectionProxy : ISubject{ private RealObject realObject; private string userRole;
public ProtectionProxy(string userRole) { this.realObject = new RealObject(); this.userRole = userRole; }
public void Operation() { if (userRole != "admin") { throw new UnauthorizedAccessException("Access denied"); } realObject.Operation(); }}package main
import "errors"
// Protection Proxy - access controltype PSubject interface { Operation() error}
type PRealObject struct{}func (r *PRealObject) Operation() error { return nil }
type ProtectionProxy struct { realObject *PRealObject userRole string}
func NewProtectionProxy(userRole string) *ProtectionProxy { return &ProtectionProxy{realObject: &PRealObject{}, userRole: userRole}}
func (p *ProtectionProxy) Operation() error { if p.userRole != "admin" { return errors.New("access denied") } return p.realObject.Operation()}// 2. Protection Proxy Access Controltrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}Use case: Access control, security
3. Remote Proxy
Section titled “3. Remote Proxy”Represents objects on remote servers:
# Remote Proxy - remote accessclass RemoteProxy: def __init__(self, server_url: str): self._server_url = server_url
def operation(self): # Make remote call response = requests.post(self._server_url, data={"operation": "do"}) return response.json()// Remote Proxy - remote accessclass RemoteProxy implements Subject { private String serverUrl;
public RemoteProxy(String serverUrl) { this.serverUrl = serverUrl; }
@Override public void operation() { // Make remote call // HTTP request to serverUrl return remoteCall(); }}import axios from 'axios';
// Remote Proxy - remote accessinterface Subject { operation(): Promise<any>;}
class RemoteProxy implements Subject { private serverUrl: string;
constructor(serverUrl: string) { this.serverUrl = serverUrl; }
async operation(): Promise<any> { // Make remote call const response = await axios.post(this.serverUrl, { operation: "do" }); return response.data; }}#include <string>
// Remote Proxy - remote accessclass Subject {public: virtual ~Subject() = default; virtual void operation() = 0;};
class RemoteProxy : public Subject {private: std::string serverUrl;
public: RemoteProxy(const std::string& serverUrl) : serverUrl(serverUrl) {}
void operation() override { // Make remote call // HTTP request to serverUrl // Use curl or other HTTP library }};using System.Net.Http;using System.Threading.Tasks;
// Remote Proxy - remote accesspublic interface ISubject{ Task OperationAsync();}
public class RemoteProxy : ISubject{ private string serverUrl;
public RemoteProxy(string serverUrl) { this.serverUrl = serverUrl; }
public async Task OperationAsync() { // Make remote call using (HttpClient client = new HttpClient()) { var content = new StringContent("{\"operation\": \"do\"}"); var response = await client.PostAsync(serverUrl, content); // Process response } }}package main
import ( "fmt" "net/http" "strings")
// Remote Proxy - remote accesstype RSubject interface { Operation() error}
type RemoteProxy struct { serverURL string}
func NewRemoteProxy(url string) *RemoteProxy { return &RemoteProxy{serverURL: url} }
func (p *RemoteProxy) Operation() error { // Make remote call resp, err := http.Post(p.serverURL, "application/json", strings.NewReader(`{"operation":"do"}`)) if err != nil { return fmt.Errorf("remote call failed: %w", err) } defer resp.Body.Close() return nil}// 3. Remote Proxytrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}Use case: Remote method invocation, RPC
4. Caching Proxy
Section titled “4. Caching Proxy”Caches results to avoid expensive operations:
# Caching Proxy - cachingclass CachingProxy: def __init__(self): self._real_object = RealObject() self._cache = {}
def operation(self, key: str): if key in self._cache: return self._cache[key] # Return cached result = self._real_object.operation(key) self._cache[key] = result # Cache result return result// Caching Proxy - cachingclass CachingProxy implements Subject { private RealSubject realSubject; private Map<String, Object> cache;
public CachingProxy() { this.realSubject = new RealSubject(); this.cache = new HashMap<>(); }
@Override public Object operation(String key) { if (cache.containsKey(key)) { return cache.get(key); // Return cached } Object result = realSubject.operation(key); cache.put(key, result); // Cache result return result; }}// Caching Proxy - cachinginterface Subject { operation(key: string): any;}
class RealObject implements Subject { operation(key: string): any { // Expensive operation return `Result for ${key}`; }}
class CachingProxy implements Subject { private realObject: RealObject; private cache: Map<string, any>;
constructor() { this.realObject = new RealObject(); this.cache = new Map(); }
operation(key: string): any { if (this.cache.has(key)) { return this.cache.get(key); // Return cached } const result = this.realObject.operation(key); this.cache.set(key, result); // Cache result return result; }}#include <string>#include <unordered_map>
// Caching Proxy - cachingclass Subject {public: virtual ~Subject() = default; virtual std::string operation(const std::string& key) = 0;};
class RealSubject : public Subject {public: std::string operation(const std::string& key) override { // Expensive operation return "Result for " + key; }};
class CachingProxy : public Subject {private: RealSubject* realSubject; std::unordered_map<std::string, std::string> cache;
public: CachingProxy() { realSubject = new RealSubject(); }
~CachingProxy() { delete realSubject; }
std::string operation(const std::string& key) override { if (cache.find(key) != cache.end()) { return cache[key]; // Return cached } std::string result = realSubject->operation(key); cache[key] = result; // Cache result return result; }};using System.Collections.Generic;
// Caching Proxy - cachingpublic interface ISubject{ object Operation(string key);}
public class RealSubject : ISubject{ public object Operation(string key) { // Expensive operation return $"Result for {key}"; }}
public class CachingProxy : ISubject{ private RealSubject realSubject; private Dictionary<string, object> cache;
public CachingProxy() { this.realSubject = new RealSubject(); this.cache = new Dictionary<string, object>(); }
public object Operation(string key) { if (cache.ContainsKey(key)) { return cache[key]; // Return cached } object result = realSubject.Operation(key); cache[key] = result; // Cache result return result; }}package main
// Caching Proxy - cachingtype CSubject interface { Operation(key string) string}
type CRealSubject struct{}func (r *CRealSubject) Operation(key string) string { return "Result for " + key }
type CachingProxy struct { realSubject *CRealSubject cache map[string]string}
func NewCachingProxy() *CachingProxy { return &CachingProxy{realSubject: &CRealSubject{}, cache: make(map[string]string)}}
func (p *CachingProxy) Operation(key string) string { if cached, ok := p.cache[key]; ok { return cached // Return cached } result := p.realSubject.Operation(key) p.cache[key] = result // Cache result return result}// 4. Caching Proxytrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}Use case: Caching, performance optimization
When to Use Proxy Pattern?
Section titled “When to Use Proxy Pattern?”Use Proxy Pattern when:
✅ You want lazy loading - Load expensive objects only when needed
✅ You want access control - Control who can access the object
✅ You want caching - Cache results to avoid expensive operations
✅ You want logging/monitoring - Track access to objects
✅ You want remote access - Access objects on remote servers
When NOT to Use Proxy Pattern?
Section titled “When NOT to Use Proxy Pattern?”Don’t use Proxy Pattern when:
❌ Simple objects - If object is simple, proxy adds unnecessary complexity
❌ No need for control - If you don’t need lazy loading, caching, or access control
❌ Performance critical - Proxy adds indirection (usually negligible)
❌ Over-engineering - Don’t add complexity for simple cases
Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”Mistake 1: Proxy Not Delegating Properly
Section titled “Mistake 1: Proxy Not Delegating Properly”# ❌ Bad: Proxy doesn't delegate to real objectclass Proxy: def operation(self): return "Proxy result" # Bad: Doesn't use real object!
# ✅ Good: Proxy delegates to real objectclass Proxy: def __init__(self): self._real_object = None
def operation(self): if self._real_object is None: self._real_object = RealObject() return self._real_object.operation() # Good: Delegates// ❌ Bad: Proxy doesn't delegate to real objectclass Proxy implements Subject { @Override public void operation() { return "Proxy result"; // Bad: Doesn't use real object! }}
// ✅ Good: Proxy delegates to real objectclass Proxy implements Subject { private RealSubject realSubject;
@Override public void operation() { if (realSubject == null) { realSubject = new RealSubject(); } return realSubject.operation(); // Good: Delegates }}// ❌ Bad: Proxy doesn't delegate to real objectinterface Subject { operation(): string;}
class RealObject implements Subject { operation(): string { return "Real result"; }}
class BadProxy implements Subject { operation(): string { return "Proxy result"; // Bad: Doesn't use real object! }}
// ✅ Good: Proxy delegates to real objectclass GoodProxy implements Subject { private realObject: RealObject | null = null;
operation(): string { if (this.realObject === null) { this.realObject = new RealObject(); } return this.realObject.operation(); // Good: Delegates }}#include <string>
// ❌ Bad: Proxy doesn't delegate to real objectclass Subject {public: virtual ~Subject() = default; virtual std::string operation() = 0;};
class RealSubject : public Subject {public: std::string operation() override { return "Real result"; }};
class BadProxy : public Subject {public: std::string operation() override { return "Proxy result"; // Bad: Doesn't use real object! }};
// ✅ Good: Proxy delegates to real objectclass GoodProxy : public Subject {private: RealSubject* realSubject = nullptr;
public: ~GoodProxy() { delete realSubject; }
std::string operation() override { if (realSubject == nullptr) { realSubject = new RealSubject(); } return realSubject->operation(); // Good: Delegates }};// ❌ Bad: Proxy doesn't delegate to real objectpublic interface ISubject{ string Operation();}
public class RealSubject : ISubject{ public string Operation() { return "Real result"; }}
public class BadProxy : ISubject{ public string Operation() { return "Proxy result"; // Bad: Doesn't use real object! }}
// ✅ Good: Proxy delegates to real objectpublic class GoodProxy : ISubject{ private RealSubject realSubject = null;
public string Operation() { if (realSubject == null) { realSubject = new RealSubject(); } return realSubject.Operation(); // Good: Delegates }}package main
type NSubject interface{ Operation() string }
type NRealSubject struct{}func (r *NRealSubject) Operation() string { return "Real result" }
// ❌ Bad: Proxy doesn't delegate to real objecttype NBadProxy struct{}func (p *NBadProxy) Operation() string { return "Proxy result" } // Bad: Doesn't use real object!
// ✅ Good: Proxy delegates to real objecttype NGoodProxy struct { realSubject *NRealSubject}func (p *NGoodProxy) Operation() string { if p.realSubject == nil { p.realSubject = &NRealSubject{} } return p.realSubject.Operation() // Good: Delegates}// Mistake 1: Proxy Not Delegating Properlystruct Image { filename: String,}impl Image { fn new(filename: &str) -> Self { println!("Loading {}", filename); Self { filename: filename.into(), } } fn display(&self) { println!("Display {}", self.filename); }}Mistake 2: Not Maintaining Same Interface
Section titled “Mistake 2: Not Maintaining Same Interface”# ❌ Bad: Proxy has different interfaceclass Proxy: def proxy_operation(self): # Bad: Different method name! pass
# ✅ Good: Proxy has same interfaceclass Proxy(Subject): def operation(self): # Good: Same interface pass// ❌ Bad: Proxy has different interfaceclass Proxy { public void proxyOperation() { // Bad: Different method name! }}
// ✅ Good: Proxy has same interfaceclass Proxy implements Subject { @Override public void operation() { // Good: Same interface }}// ❌ Bad: Proxy has different interfaceinterface Subject { operation(): void;}
class BadProxy { proxyOperation(): void { // Bad: Different method name! }}
// ✅ Good: Proxy has same interfaceclass GoodProxy implements Subject { operation(): void { // Good: Same interface }}// ❌ Bad: Proxy has different interfaceclass Subject {public: virtual ~Subject() = default; virtual void operation() = 0;};
class BadProxy {public: void proxyOperation() { // Bad: Different method name! }};
// ✅ Good: Proxy has same interfaceclass GoodProxy : public Subject {public: void operation() override { // Good: Same interface }};// ❌ Bad: Proxy has different interfacepublic interface ISubject{ void Operation();}
public class BadProxy{ public void ProxyOperation() // Bad: Different method name! { }}
// ✅ Good: Proxy has same interfacepublic class GoodProxy : ISubject{ public void Operation() // Good: Same interface { }}package main
type DSubject interface{ Operation() }
// ❌ Bad: Proxy has different interfacetype DBadProxy struct{}func (p *DBadProxy) ProxyOperation() {} // Bad: Different method name!
// ✅ Good: Proxy has same interfacetype DGoodProxy struct{}func (p *DGoodProxy) Operation() {} // Good: Same interface// Mistake 2: Not Maintaining Same Interfacestruct Image { filename: String,}impl Image { fn new(filename: &str) -> Self { println!("Loading {}", filename); Self { filename: filename.into(), } } fn display(&self) { println!("Display {}", self.filename); }}Mistake 3: Creating Real Object Unnecessarily
Section titled “Mistake 3: Creating Real Object Unnecessarily”# ❌ Bad: Creates real object even if not neededclass Proxy: def __init__(self): self._real_object = RealObject() # Bad: Created immediately!
def operation(self): return self._real_object.operation()
# ✅ Good: Creates real object only when neededclass Proxy: def __init__(self): self._real_object = None # Good: Not created yet
def operation(self): if self._real_object is None: self._real_object = RealObject() # Good: Created on demand return self._real_object.operation()// ❌ Bad: Creates real object even if not neededclass Proxy implements Subject { private RealSubject realSubject = new RealSubject(); // Bad: Created immediately!
@Override public void operation() { return realSubject.operation(); }}
// ✅ Good: Creates real object only when neededclass Proxy implements Subject { private RealSubject realSubject; // Good: Not created yet
@Override public void operation() { if (realSubject == null) { realSubject = new RealSubject(); // Good: Created on demand } return realSubject.operation(); }}// ❌ Bad: Creates real object even if not neededinterface Subject { operation(): void;}
class RealObject implements Subject { operation(): void { // Expensive operation }}
class BadProxy implements Subject { private realObject: RealObject = new RealObject(); // Bad: Created immediately!
operation(): void { return this.realObject.operation(); }}
// ✅ Good: Creates real object only when neededclass GoodProxy implements Subject { private realObject: RealObject | null = null; // Good: Not created yet
operation(): void { if (this.realObject === null) { this.realObject = new RealObject(); // Good: Created on demand } return this.realObject.operation(); }}// ❌ Bad: Creates real object even if not neededclass Subject {public: virtual ~Subject() = default; virtual void operation() = 0;};
class RealSubject : public Subject {public: RealSubject() { // Expensive initialization }
void operation() override { // Operation }};
class BadProxy : public Subject {private: RealSubject* realSubject = new RealSubject(); // Bad: Created immediately!
public: ~BadProxy() { delete realSubject; }
void operation() override { realSubject->operation(); }};
// ✅ Good: Creates real object only when neededclass GoodProxy : public Subject {private: RealSubject* realSubject = nullptr; // Good: Not created yet
public: ~GoodProxy() { delete realSubject; }
void operation() override { if (realSubject == nullptr) { realSubject = new RealSubject(); // Good: Created on demand } realSubject->operation(); }};// ❌ Bad: Creates real object even if not neededpublic interface ISubject{ void Operation();}
public class RealSubject : ISubject{ public RealSubject() { // Expensive initialization }
public void Operation() { // Operation }}
public class BadProxy : ISubject{ private RealSubject realSubject = new RealSubject(); // Bad: Created immediately!
public void Operation() { realSubject.Operation(); }}
// ✅ Good: Creates real object only when neededpublic class GoodProxy : ISubject{ private RealSubject realSubject = null; // Good: Not created yet
public void Operation() { if (realSubject == null) { realSubject = new RealSubject(); // Good: Created on demand } realSubject.Operation(); }}package main
type USubject interface{ Operation() }
type URealSubject struct{} // Expensive initializationfunc (r *URealSubject) Operation() {}
// ❌ Bad: Creates real object even if not neededtype UBadProxy struct { realSubject *URealSubject}func NewUBadProxy() *UBadProxy { return &UBadProxy{realSubject: &URealSubject{}} // Bad: Created immediately!}func (p *UBadProxy) Operation() { p.realSubject.Operation() }
// ✅ Good: Creates real object only when neededtype UGoodProxy struct { realSubject *URealSubject // nil until needed}func (p *UGoodProxy) Operation() { if p.realSubject == nil { p.realSubject = &URealSubject{} // Good: Created on demand } p.realSubject.Operation()}// Mistake 3: Creating Real Object Unnecessarilystruct Image { filename: String,}impl Image { fn new(filename: &str) -> Self { println!("Loading {}", filename); Self { filename: filename.into(), } } fn display(&self) { println!("Display {}", self.filename); }}Benefits of Proxy Pattern
Section titled “Benefits of Proxy Pattern”- Lazy Loading - Loads expensive objects only when needed
- Access Control - Controls who can access objects
- Caching - Caches results to avoid expensive operations
- Remote Access - Accesses objects on remote servers
- Separation of Concerns - Adds cross-cutting concerns without modifying real object
- Transparent - Client doesn’t know it’s using a proxy
Revision: Quick Catch-Up
Section titled “Revision: Quick Catch-Up”What is Proxy Pattern?
Section titled “What is Proxy Pattern?”Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. The proxy has the same interface as the real object.
Why Use It?
Section titled “Why Use It?”- ✅ Lazy loading - Load expensive objects only when needed
- ✅ Access control - Control who can access objects
- ✅ Caching - Cache results to avoid expensive operations
- ✅ Remote access - Access objects on remote servers
- ✅ Separation of concerns - Add cross-cutting concerns
How It Works?
Section titled “How It Works?”- Define subject interface - Common interface for proxy and real object
- Implement real subject - The actual object
- Create proxy - Implements same interface, controls access
- Client uses proxy - Client interacts with proxy (transparent)
- Proxy delegates - Proxy delegates to real object when needed
Key Components
Section titled “Key Components”Client → Proxy → RealSubject- Subject - Interface for proxy and real object
- RealSubject - The actual object
- Proxy - Controls access to real subject
- Client - Uses proxy (doesn’t know it’s a proxy)
Simple Example
Section titled “Simple Example”class Proxy(Subject): def __init__(self): self._real_subject = None
def operation(self): if self._real_subject is None: self._real_subject = RealSubject() # Lazy load return self._real_subject.operation()class Proxy implements Subject { private RealSubject realSubject;
public String operation() { if (realSubject == null) realSubject = new RealSubject(); // Lazy load return realSubject.operation(); }}class Proxy implements Subject { private realSubject: RealSubject | null = null;
operation(): string { if (!this.realSubject) this.realSubject = new RealSubject(); // Lazy load return this.realSubject.operation(); }}class Proxy : public Subject { RealSubject* realSubject = nullptr;public: std::string operation() { if (!realSubject) realSubject = new RealSubject(); // Lazy load return realSubject->operation(); }};class Proxy : ISubject { private RealSubject realSubject;
public string Operation() { if (realSubject == null) realSubject = new RealSubject(); // Lazy load return realSubject.Operation(); }}type Subject interface{ Operation() string }
type RealSubject struct{}func (r *RealSubject) Operation() string { return "real" }
type Proxy struct{ realSubject *RealSubject }func (p *Proxy) Operation() string { if p.realSubject == nil { p.realSubject = &RealSubject{} // Lazy load } return p.realSubject.Operation()}// Simple Exampletrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}When to Use?
Section titled “When to Use?”✅ Lazy loading
✅ Access control
✅ Caching
✅ Remote access
✅ Logging/monitoring
When NOT to Use?
Section titled “When NOT to Use?”❌ Simple objects
❌ No need for control
❌ Performance critical
❌ Over-engineering
Key Takeaways
Section titled “Key Takeaways”- Proxy Pattern = Controls access to real object
- Proxy = Placeholder with same interface
- RealSubject = Actual object
- Benefit = Lazy loading, caching, access control
- Use Case = Expensive resources, caching, security
Common Pattern Structure
Section titled “Common Pattern Structure”class Proxy(Subject): def __init__(self): self._real_subject = None
def operation(self): if self._real_subject is None: self._real_subject = RealSubject() return self._real_subject.operation()class Proxy implements Subject { private RealSubject realSubject;
public String operation() { if (realSubject == null) realSubject = new RealSubject(); return realSubject.operation(); }}class Proxy implements Subject { private realSubject: RealSubject | null = null;
operation(): string { if (!this.realSubject) this.realSubject = new RealSubject(); return this.realSubject.operation(); }}class Proxy : public Subject { RealSubject* realSubject = nullptr;public: std::string operation() { if (!realSubject) realSubject = new RealSubject(); return realSubject->operation(); }};class Proxy : ISubject { private RealSubject realSubject;
public string Operation() { if (realSubject == null) realSubject = new RealSubject(); return realSubject.Operation(); }}type Subject interface{ Operation() string }
type RealSubject struct{}func (r *RealSubject) Operation() string { return "real" }
type Proxy struct{ realSubject *RealSubject }func (p *Proxy) Operation() string { if p.realSubject == nil { p.realSubject = &RealSubject{} } return p.realSubject.Operation()}// Common Pattern Structuretrait Image { fn display(&mut self);}struct RealImage { filename: String,}impl RealImage { fn new(filename: &str) -> Self { println!("load {}", filename); Self { filename: filename.into(), } }}impl Image for RealImage { fn display(&mut self) { println!("display {}", self.filename); }}struct ImageProxy { filename: String, real: Option<RealImage>,}impl Image for ImageProxy { fn display(&mut self) { let real = self .real .get_or_insert_with(|| RealImage::new(&self.filename)); real.display(); }}Remember
Section titled “Remember”- Proxy Pattern controls access to real objects
- It has the same interface as real object
- It’s about control, not just wrapping!
- Use it for lazy loading, caching, access control
- Keep it transparent - client shouldn’t know it’s a proxy!
Interview Focus: Proxy Pattern
Section titled “Interview Focus: Proxy Pattern”Key Points to Remember
Section titled “Key Points to Remember”1. Core Concept
Section titled “1. Core Concept”What to say:
“Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. The proxy has the same interface as the real object, allowing it to be used interchangeably while adding functionality like lazy loading, caching, or access control.”
Why it matters:
- Shows you understand the fundamental purpose
- Demonstrates knowledge of when to use it
- Indicates you can explain concepts clearly
2. When to Use Proxy Pattern
Section titled “2. When to Use Proxy Pattern”Must mention:
- ✅ Lazy loading - Load expensive objects only when needed
- ✅ Access control - Control who can access objects
- ✅ Caching - Cache results to avoid expensive operations
- ✅ Remote access - Access objects on remote servers
- ✅ Logging/monitoring - Track access to objects
Example scenario to give:
“I’d use Proxy Pattern when building an image viewer. Loading images from disk is expensive. Instead of loading all images upfront, I create image proxies that load images only when they’re actually displayed. This improves startup time and reduces memory usage.”
Must discuss:
- Proxy: Controls access, same interface, adds functionality like lazy loading/caching
- Decorator: Adds behavior, same interface, wraps to enhance functionality
- Key difference: Proxy controls access, Decorator adds behavior
Example to give:
“Proxy Pattern controls access to an object and adds functionality like lazy loading or caching. Decorator Pattern adds behavior to an object dynamically. Proxy is about control and optimization, Decorator is about adding features. Proxy typically has one real object, Decorator can wrap multiple decorators.”
4. Proxy Variants
Section titled “4. Proxy Variants”Must discuss:
- Virtual Proxy: Lazy loading of expensive resources
- Protection Proxy: Access control and security
- Remote Proxy: Remote object access
- Caching Proxy: Caching expensive operations
Example to give:
“I use Virtual Proxy for lazy loading images, Protection Proxy for access control in a secure system, Remote Proxy for accessing objects on remote servers, and Caching Proxy for caching database query results. Each variant serves a specific purpose.”
5. Benefits and Trade-offs
Section titled “5. Benefits and Trade-offs”Benefits to mention:
- Lazy loading - Loads expensive objects only when needed
- Access control - Controls who can access objects
- Caching - Caches results to avoid expensive operations
- Separation of concerns - Adds cross-cutting concerns without modifying real object
- Transparent - Client doesn’t know it’s using a proxy
Trade-offs to acknowledge:
- Complexity - Adds abstraction layer
- Performance - Small overhead (usually negligible)
- Over-engineering risk - Can be overkill for simple cases
6. Common Interview Questions
Section titled “6. Common Interview Questions”Q: “What’s the difference between Proxy Pattern and Decorator Pattern?”
A:
“Proxy Pattern controls access to an object and adds functionality like lazy loading, caching, or access control. Decorator Pattern adds behavior to an object dynamically. Proxy is about control and optimization, Decorator is about adding features. Proxy typically manages one real object, Decorator can wrap multiple decorators in a chain.”
Q: “When would you use Virtual Proxy vs Caching Proxy?”
A:
“I use Virtual Proxy when I want to defer the creation of expensive objects until they’re actually needed - like loading images only when displayed. I use Caching Proxy when I want to cache the results of expensive operations - like caching database query results to avoid repeated queries.”
Q: “How does Proxy Pattern relate to SOLID principles?”
A:
“Proxy Pattern supports the Single Responsibility Principle by separating access control, caching, or lazy loading into the proxy. It supports the Open/Closed Principle - you can add functionality (proxy) without modifying the real object. It supports the Dependency Inversion Principle by having clients depend on the Subject interface rather than concrete implementations.”
Interview Checklist
Section titled “Interview Checklist”Before your interview, make sure you can:
- Define Proxy Pattern clearly in one sentence
- Explain when to use it (with examples showing lazy loading/caching)
- Describe Proxy vs Decorator Pattern
- Implement Proxy Pattern from scratch
- Compare with other structural patterns (Decorator, Adapter)
- List proxy variants (Virtual, Protection, Remote, Caching)
- List benefits and trade-offs
- Identify common mistakes (not delegating, different interface)
- Give 2-3 real-world examples
- Connect to SOLID principles
- Discuss when NOT to use it
Remember: Proxy Pattern is about controlling access to objects - providing a placeholder that adds functionality like lazy loading, caching, or access control! 🎯