20 minlesson

Introduction to Proxy Pattern

Introduction to Proxy Pattern

Proxy provides a surrogate or placeholder for another object to control access to it.

The Problem

Direct access to external APIs can be problematic:

typescript
1// Direct API calls without control
2class ShippingClient {
3 async getRates(request: RateRequest): Promise<Rate[]> {
4 // Every call hits the external API
5 // No rate limiting
6 // No caching
7 // No logging
8 // No access control
9 return await fetch(`${API_URL}/rates`, { body: JSON.stringify(request) });
10 }
11}
12
13// Issues:
14// - API overload leads to rate limiting/bans
15// - Redundant calls for same data
16// - No visibility into usage
17// - No protection from abuse

The Solution: Proxy

Add a layer of control:

typescript
1interface ShippingService {
2 getRates(request: RateRequest): Promise<Rate[]>;
3}
4
5// Real subject
6class CarrierAPI implements ShippingService {
7 async getRates(request: RateRequest): Promise<Rate[]> {
8 return await fetch(`${API_URL}/rates`, { body: JSON.stringify(request) });
9 }
10}
11
12// Proxy with rate limiting
13class RateLimitedProxy implements ShippingService {
14 private lastCall = 0;
15 private minInterval = 1000; // 1 second between calls
16
17 constructor(private real: ShippingService) {}
18
19 async getRates(request: RateRequest): Promise<Rate[]> {
20 const now = Date.now();
21 const elapsed = now - this.lastCall;
22
23 if (elapsed < this.minInterval) {
24 await this.sleep(this.minInterval - elapsed);
25 }
26
27 this.lastCall = Date.now();
28 return this.real.getRates(request);
29 }
30
31 private sleep(ms: number): Promise<void> {
32 return new Promise(resolve => setTimeout(resolve, ms));
33 }
34}
35
36// Usage - transparent to client
37const service: ShippingService = new RateLimitedProxy(new CarrierAPI());
38const rates = await service.getRates(request);

Pattern Structure

1┌─────────────────────┐
2│ Client │
3└──────────┬──────────┘
4
5
6┌─────────────────────┐
7│ Subject │
8│ (interface) │
9│─────────────────────│
10│ + request() │
11└──────────┬──────────┘
12
13 ┌─────┴─────┐
14 │ │
15┌────▼────┐ ┌────▼────┐
16│ Proxy │ │ Real │
17│─────────│ │ Subject │
18│- real │─│─────────│
19│+request()│ │+request()│
20└─────────┘ └─────────┘

Types of Proxies

Virtual Proxy (Lazy Loading)

typescript
1class LazyCarrierProxy implements ShippingService {
2 private real: CarrierAPI | null = null;
3
4 async getRates(request: RateRequest): Promise<Rate[]> {
5 if (!this.real) {
6 // Expensive initialization only when needed
7 this.real = await this.initializeCarrier();
8 }
9 return this.real.getRates(request);
10 }
11
12 private async initializeCarrier(): Promise<CarrierAPI> {
13 // Load credentials, establish connection, etc.
14 return new CarrierAPI();
15 }
16}

Protection Proxy (Access Control)

typescript
1class SecuredCarrierProxy implements ShippingService {
2 constructor(
3 private real: ShippingService,
4 private authService: AuthService
5 ) {}
6
7 async getRates(request: RateRequest): Promise<Rate[]> {
8 const user = await this.authService.getCurrentUser();
9
10 if (!user.hasPermission('shipping:read')) {
11 throw new UnauthorizedError('Cannot access shipping rates');
12 }
13
14 return this.real.getRates(request);
15 }
16}

Caching Proxy

typescript
1class CachingCarrierProxy implements ShippingService {
2 private cache = new Map<string, { data: Rate[]; expires: number }>();
3
4 constructor(
5 private real: ShippingService,
6 private ttl: number = 300000 // 5 minutes
7 ) {}
8
9 async getRates(request: RateRequest): Promise<Rate[]> {
10 const key = this.getCacheKey(request);
11 const cached = this.cache.get(key);
12
13 if (cached && cached.expires > Date.now()) {
14 return cached.data;
15 }
16
17 const rates = await this.real.getRates(request);
18 this.cache.set(key, { data: rates, expires: Date.now() + this.ttl });
19 return rates;
20 }
21
22 private getCacheKey(request: RateRequest): string {
23 return JSON.stringify(request);
24 }
25}

Logging Proxy

typescript
1class LoggingCarrierProxy implements ShippingService {
2 constructor(
3 private real: ShippingService,
4 private logger: Logger
5 ) {}
6
7 async getRates(request: RateRequest): Promise<Rate[]> {
8 const start = Date.now();
9 this.logger.info('Getting rates', { request });
10
11 try {
12 const rates = await this.real.getRates(request);
13 this.logger.info('Rates received', {
14 duration: Date.now() - start,
15 rateCount: rates.length
16 });
17 return rates;
18 } catch (error) {
19 this.logger.error('Rate lookup failed', { error, duration: Date.now() - start });
20 throw error;
21 }
22 }
23}

When to Use Proxy

TypeUse Case
VirtualExpensive object creation
ProtectionAccess control
CachingRepeated expensive operations
LoggingAudit trail, debugging
Rate LimitingAPI protection
RemoteNetwork transparency

Summary

Proxy controls access to an object without clients knowing. It's transparent - same interface as the real object - but adds valuable behaviors like caching, logging, and protection.