Design Principle
Proxy Pattern
Learn the Proxy pattern: provide a surrogate or placeholder for another object to control access to it. Understand virtual, protection, and remote proxies.
The Proxy Pattern provides a surrogate or placeholder for another object to control access to it. The proxy acts as an intermediary between the client and the real object.
What is the Proxy Pattern?
Proxy Pattern provides:
- Access control: Control access to the real object
- Lazy loading: Defer expensive object creation
- Caching: Cache results from expensive operations
- Security: Add security checks before access
Types:
- Virtual Proxy: Lazy loading
- Protection Proxy: Access control
- Remote Proxy: Network communication
- Caching Proxy: Cache results
Structure
Subject (interface)
└─ request()
RealSubject (implements Subject)
└─ request()
Proxy (implements Subject)
└─ realSubject: RealSubject
└─ request() (controls access, calls realSubject.request())
Examples
Virtual Proxy (Lazy Loading)
// Subject interface
interface Image {
display(): void;
}
// Real subject
class RealImage implements Image {
private filename: string;
constructor(filename: string) {
this.filename = filename;
this.loadFromDisk(); // Expensive operation
}
private loadFromDisk(): void {
console.log(`Loading ${this.filename} from disk...`);
// Simulate expensive loading
}
display(): void {
console.log(`Displaying ${this.filename}`);
}
}
// Proxy (lazy loading)
class ProxyImage implements Image {
private realImage: RealImage | null = null;
private filename: string;
constructor(filename: string) {
this.filename = filename;
}
display(): void {
if (this.realImage === null) {
this.realImage = new RealImage(this.filename); // Lazy loading
}
this.realImage.display();
}
}
// Usage
const image = new ProxyImage("photo.jpg");
// Image not loaded yet
image.display();
// Now image is loaded and displayed
Protection Proxy (Access Control)
// Subject interface
interface BankAccount {
deposit(amount: number): void;
withdraw(amount: number): void;
getBalance(): number;
}
// Real subject
class RealBankAccount implements BankAccount {
private balance: number = 0;
deposit(amount: number): void {
this.balance += amount;
console.log(`Deposited ${amount}. Balance: ${this.balance}`);
}
withdraw(amount: number): void {
if (this.balance >= amount) {
this.balance -= amount;
console.log(`Withdrew ${amount}. Balance: ${this.balance}`);
} else {
console.log("Insufficient funds");
}
}
getBalance(): number {
return this.balance;
}
}
// Protection proxy
class ProtectionProxy implements BankAccount {
private realAccount: RealBankAccount;
private user: User;
constructor(user: User) {
this.user = user;
this.realAccount = new RealBankAccount();
}
deposit(amount: number): void {
if (this.hasPermission("deposit")) {
this.realAccount.deposit(amount);
} else {
throw new Error("Permission denied");
}
}
withdraw(amount: number): void {
if (this.hasPermission("withdraw")) {
this.realAccount.withdraw(amount);
} else {
throw new Error("Permission denied");
}
}
getBalance(): number {
if (this.hasPermission("view")) {
return this.realAccount.getBalance();
} else {
throw new Error("Permission denied");
}
}
private hasPermission(action: string): boolean {
// Check user permissions
return this.user.permissions.includes(action);
}
}
// Usage
const user = { permissions: ["deposit", "view"] };
const account = new ProtectionProxy(user);
account.deposit(100); // Allowed
account.getBalance(); // Allowed
account.withdraw(50); // Throws error (no permission)
Caching Proxy
// Subject interface
interface DataService {
fetchData(key: string): Promise<string>;
}
// Real subject
class RealDataService implements DataService {
async fetchData(key: string): Promise<string> {
console.log(`Fetching data for key: ${key}`);
// Simulate expensive network call
await new Promise(resolve => setTimeout(resolve, 1000));
return `Data for ${key}`;
}
}
// Caching proxy
class CachingProxy implements DataService {
private realService: RealDataService;
private cache: Map<string, string> = new Map();
constructor() {
this.realService = new RealDataService();
}
async fetchData(key: string): Promise<string> {
// Check cache first
if (this.cache.has(key)) {
console.log(`Cache hit for key: ${key}`);
return this.cache.get(key)!;
}
// Cache miss - fetch from real service
console.log(`Cache miss for key: ${key}`);
const data = await this.realService.fetchData(key);
this.cache.set(key, data);
return data;
}
clearCache(): void {
this.cache.clear();
}
}
// Usage
const service = new CachingProxy();
// First call - cache miss
await service.fetchData("key1");
// Second call - cache hit
await service.fetchData("key1");
Remote Proxy
// Subject interface
interface RemoteService {
processRequest(data: string): Promise<string>;
}
// Real subject (on remote server)
class RemoteServiceImpl implements RemoteService {
async processRequest(data: string): Promise<string> {
// Process on remote server
return `Processed: ${data}`;
}
}
// Remote proxy (client-side)
class RemoteProxy implements RemoteService {
private serverUrl: string;
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
}
async processRequest(data: string): Promise<string> {
// Make network call to remote server
const response = await fetch(`${this.serverUrl}/process`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data })
});
const result = await response.json();
return result.processed;
}
}
// Usage
const proxy = new RemoteProxy("https://api.example.com");
const result = await proxy.processRequest("test data");
Common Pitfalls
- Over-proxying: Too many proxy layers. Fix: Use only when needed
- Performance overhead: Proxy adds overhead. Fix: Consider performance impact
- Tight coupling: Proxy tightly coupled to real subject. Fix: Use interfaces
- Not handling errors: Proxy doesn't handle errors properly. Fix: Add error handling
Interview Questions
Beginner
Q: What is the Proxy pattern and what are its use cases?
A:
Proxy Pattern provides a surrogate or placeholder for another object to control access to it.
Key characteristics:
- Surrogate: Acts as intermediary between client and real object
- Access control: Controls access to real object
- Same interface: Implements same interface as real subject
Types:
- Virtual Proxy: Lazy loading (defer expensive creation)
- Protection Proxy: Access control (security checks)
- Remote Proxy: Network communication (remote objects)
- Caching Proxy: Cache results (avoid repeated operations)
Use cases:
- Lazy loading: Defer expensive object creation
- Access control: Add security checks
- Caching: Cache expensive operations
- Remote access: Access remote objects locally
Example:
// Proxy controls access to real image
class ProxyImage implements Image {
private realImage: RealImage | null = null;
display(): void {
if (this.realImage === null) {
this.realImage = new RealImage(); // Lazy loading
}
this.realImage.display();
}
}
Intermediate
Q: Explain different types of proxies. How do you implement a virtual proxy for lazy loading?
A:
Types of Proxies:
1. Virtual Proxy (Lazy Loading):
class ProxyImage implements Image {
private realImage: RealImage | null = null;
display(): void {
if (this.realImage === null) {
this.realImage = new RealImage(); // Load only when needed
}
this.realImage.display();
}
}
Use: Defer expensive object creation
2. Protection Proxy (Access Control):
class ProtectionProxy implements BankAccount {
deposit(amount: number): void {
if (this.hasPermission()) {
this.realAccount.deposit(amount);
} else {
throw new Error("Permission denied");
}
}
}
Use: Add security checks
3. Remote Proxy:
class RemoteProxy implements Service {
async process(data: string): Promise<string> {
// Make network call
return await fetch("/api/process", { body: data });
}
}
Use: Access remote objects locally
4. Caching Proxy:
class CachingProxy implements DataService {
async fetch(key: string): Promise<string> {
if (this.cache.has(key)) {
return this.cache.get(key); // Return cached
}
const data = await this.realService.fetch(key);
this.cache.set(key, data);
return data;
}
}
Use: Cache expensive operations
Senior
Q: Design a proxy system for a distributed caching layer that handles cache invalidation, load balancing, and failover. How do you ensure consistency and handle network failures?
A:
// Subject interface
interface CacheService {
get(key: string): Promise<string | null>;
set(key: string, value: string, ttl: number): Promise<void>;
delete(key: string): Promise<void>;
}
// Real cache service (Redis, Memcached, etc.)
class RealCacheService implements CacheService {
async get(key: string): Promise<string | null> {
// Direct cache access
return await this.cache.get(key);
}
async set(key: string, value: string, ttl: number): Promise<void> {
await this.cache.set(key, value, ttl);
}
async delete(key: string): Promise<void> {
await this.cache.delete(key);
}
}
// Distributed cache proxy
class DistributedCacheProxy implements CacheService {
private servers: CacheService[];
private currentServer: number = 0;
private localCache: Map<string, { value: string, expires: number }> = new Map();
constructor(servers: CacheService[]) {
this.servers = servers;
}
async get(key: string): Promise<string | null> {
// Check local cache first
const local = this.localCache.get(key);
if (local && local.expires > Date.now()) {
return local.value;
}
// Try servers with failover
for (let i = 0; i < this.servers.length; i++) {
const server = this.getServer();
try {
const value = await this.withTimeout(
server.get(key),
1000 // 1 second timeout
);
if (value !== null) {
// Cache locally
this.localCache.set(key, {
value,
expires: Date.now() + 60000 // 1 minute local cache
});
return value;
}
} catch (error) {
// Server failed, try next
this.rotateServer();
continue;
}
}
return null;
}
async set(key: string, value: string, ttl: number): Promise<void> {
// Write to all servers (for consistency)
const promises = this.servers.map(server =>
this.withTimeout(server.set(key, value, ttl), 1000)
.catch(error => {
console.error(`Server write failed: ${error}`);
})
);
await Promise.all(promises);
// Update local cache
this.localCache.set(key, {
value,
expires: Date.now() + ttl * 1000
});
}
async delete(key: string): Promise<void> {
// Delete from all servers
await Promise.all(
this.servers.map(server =>
this.withTimeout(server.delete(key), 1000)
)
);
// Remove from local cache
this.localCache.delete(key);
}
private getServer(): CacheService {
// Round-robin load balancing
return this.servers[this.currentServer];
}
private rotateServer(): void {
this.currentServer = (this.currentServer + 1) % this.servers.length;
}
private async withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout)
)
]);
}
// Cache invalidation
invalidate(pattern: string): void {
// Invalidate matching keys
for (const key of this.localCache.keys()) {
if (this.matchesPattern(key, pattern)) {
this.localCache.delete(key);
}
}
}
private matchesPattern(key: string, pattern: string): boolean {
// Simple pattern matching
const regex = new RegExp(pattern.replace("*", ".*"));
return regex.test(key);
}
}
// Usage
const servers = [
new RealCacheService("redis://server1"),
new RealCacheService("redis://server2"),
new RealCacheService("redis://server3")
];
const proxy = new DistributedCacheProxy(servers);
await proxy.set("key1", "value1", 3600);
const value = await proxy.get("key1");
Features:
- Load balancing: Round-robin across servers
- Failover: Try next server on failure
- Local caching: Cache locally for performance
- Consistency: Write to all servers
- Timeout handling: Handle network timeouts
- Cache invalidation: Invalidate by pattern
Key Takeaways
- Proxy pattern: Provides surrogate to control access to object
- Types: Virtual (lazy loading), Protection (access control), Remote (network), Caching (cache)
- Benefits: Access control, lazy loading, caching, security
- Use cases: Lazy loading, access control, remote access, caching
- Best practices: Use interfaces, handle errors, consider performance
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.