Proxy
Provide a substitute for another object. A proxy intercepts access to the real object, letting you perform actions before or after the request — caching, access control, logging, lazy loading.
The Problem
You have a Database object. Some operations are expensive — loading a large dataset takes 3 seconds. You call it 10 times in a request loop, each time starting from scratch. Or you have a service that should only be accessible to admin users. Or you want to log every call without modifying the service class.
Adding caching/access-control/logging directly to the service violates the Single Responsibility Principle. And you can’t always modify the original class (third-party code).
The Solution
Create a Proxy that implements the same interface as the real object. The Proxy wraps the real object and intercepts calls, adding its extra behaviour before/after forwarding.
Real-World Analogy
A credit card is a proxy for your bank account. You use the same interface (tap to pay) whether you’re using cash or a card. The card (proxy) performs extra work: authentication, fraud checks, logging — before money actually moves from your account. You don’t think about this; you just “pay.”
TypeScript Example
// Subject interface — both Proxy and RealSubject implement this
interface DataService {
fetchUser(id: number): Promise<{ id: number; name: string }>;
updateUser(id: number, name: string): Promise<void>;
}
// Real Subject — the actual implementation
class UserDataService implements DataService {
async fetchUser(id: number): Promise<{ id: number; name: string }> {
console.log(`[DB] Querying user ${id} (slow operation)`);
await new Promise(r => setTimeout(r, 100)); // simulate latency
return { id, name: `User ${id}` };
}
async updateUser(id: number, name: string): Promise<void> {
console.log(`[DB] Updating user ${id} → "${name}"`);
await new Promise(r => setTimeout(r, 80));
}
}
// Caching + Logging Proxy
class DataServiceProxy implements DataService {
private readonly cache = new Map<number, { id: number; name: string }>();
private callLog: string[] = [];
constructor(private readonly real: DataService) {}
async fetchUser(id: number): Promise<{ id: number; name: string }> {
this.callLog.push(`fetchUser(${id}) at ${new Date().toISOString()}`);
if (this.cache.has(id)) {
console.log(`[Cache] HIT for user ${id}`);
return this.cache.get(id)!;
}
console.log(`[Cache] MISS for user ${id} — delegating`);
const user = await this.real.fetchUser(id);
this.cache.set(id, user);
return user;
}
async updateUser(id: number, name: string): Promise<void> {
this.callLog.push(`updateUser(${id}, "${name}") at ${new Date().toISOString()}`);
this.cache.delete(id); // invalidate cache on write
await this.real.updateUser(id, name);
}
getLog(): string[] { return [...this.callLog]; }
}
// Protection Proxy — wraps caching proxy with auth check
class AuthenticatedDataService implements DataService {
constructor(
private readonly inner: DataService,
private readonly userRole: 'admin' | 'user' | 'guest'
) {}
async fetchUser(id: number): Promise<{ id: number; name: string }> {
if (this.userRole === 'guest') throw new Error('Unauthorised: guests cannot read user data');
return this.inner.fetchUser(id);
}
async updateUser(id: number, name: string): Promise<void> {
if (this.userRole !== 'admin') throw new Error('Unauthorised: only admins can update users');
return this.inner.updateUser(id, name);
}
}
// Wire it up — proxies compose cleanly
async function main() {
const real = new UserDataService();
const cached = new DataServiceProxy(real);
const service = new AuthenticatedDataService(cached, 'admin');
const u1 = await service.fetchUser(42); // [Cache] MISS → [DB] query
console.log(u1); // { id: 42, name: 'User 42' }
const u2 = await service.fetchUser(42); // [Cache] HIT — no DB call
console.log(u2); // { id: 42, name: 'User 42' }
await service.updateUser(42, 'Alice'); // invalidates cache
const u3 = await service.fetchUser(42); // [Cache] MISS again → DB
console.log(u3);
console.log(cached.getLog());
}
main();
When to Use
- Virtual Proxy — delay expensive object creation until it’s actually needed
- Caching Proxy — cache results of repeated expensive operations
- Protection Proxy — gate access based on permissions
- Logging Proxy — log, audit, or profile calls transparently
- Remote Proxy — make a remote service feel like a local object (RPC, REST)
Pros
- Adds behaviour without modifying the real subject
- Proxies compose — stack a caching proxy inside an auth proxy inside a logging proxy
- Can control object lifecycle (virtual proxy manages creation/destruction)
- Open/Closed Principle — add new proxy types without changing the subject
Cons
- Adds a layer of indirection — slightly more complex call stack
- Response time increases (auth checks, cache lookups) — though often offset by what the proxy saves
- If the proxy and real subject get out of sync (e.g., interface change), bugs can be subtle
Related Patterns
- Decorator adds behaviour and keeps the interface; Proxy also keeps the interface but controls access to the real object
- Adapter changes the interface; Proxy keeps it
- Facade simplifies a complex API; Proxy wraps a single object