Low-Level Design: Proxy Design Pattern
The Proxy Design Pattern is a structural design pattern that provides a surrogate, placeholder, or gatekeeper object for another target object to control access to it. By interposing a proxy between a client and the actual resource, software architects can enforce security, execute rate limiting, implement response caching, and defer expensive object creation without modifying the core business logic.
1. Introduction & Real-World Intuition
At its core, a Proxy acts as an intermediary controlling access to a target resource or subject. In software architecture, clients do not interact directly with the underlying high-cost or sensitive service; instead, they interact with a proxy that mirrors the service interface while adding structural control.
Real-Life Analogy
Real-Life Analogy
- The Classroom Proxy Attendance: Imagine a student who cannot attend a lecture asking a classmate to mark attendance on their behalf. The classmate acts as a proxy, standing in for the actual student during the roll call.
- The Friend Intercepting a Message: Suppose you are furious and want to yell cuss words at your university professor. Realizing this will result in your expulsion, your friend intercepts your message and converts your angry complaint into polite, constructive feedback. Your friend acts as a proxy gatekeeper—filtering, controlling, and modifying the interaction before it reaches the target (the professor) to prevent disaster.
Client / Student
|
| sends unfiltered request / message
v
Proxy (Classmate / Friend)
|
| inspects, filters, and sanitizes request
v
Real Subject (Professor / Target System)
2. The Naive Approach & System Pitfalls
Consider building a video streaming infrastructure similar to Netflix. The system exposes a core backend component, VideoService, responsible for loading, rendering, and streaming video files.
Naive Architecture (Direct Interaction)
Without a proxy layer, client applications communicate directly with the core VideoService:
User Browser / Mobile App
|
| direct API request to stream video
v
VideoService (Core Monolithic Logic)
Pitfalls of Direct Access
When clients hit the core service directly, the VideoService must handle every cross-cutting concern alongside its core streaming responsibilities:
- Unauthorized Access & Bypassing Paywalls: Without an enforcing barrier, free tier or unauthenticated users can manipulate requests to bypass authorization logic and access premium content directly.
- System Overload and Abuse (DDoS/Bot Traffic): Malicious actors or runaway client scripts can bombard the core service with repetitive requests. Lacking an external gatekeeper, the backend service gets overwhelmed, rendering it unable to serve legitimate traffic.
- High Bandwidth & Compute Costs: Serving every request directly from the primary media server strains network resources. Popular or trending videos are continuously fetched and processed from scratch instead of being served from an edge caching layer.
Naive Implementation (Code Example)
// Naive approach: Mixing business logic with access control and infrastructure concerns
public class NaiveVideoService {
public void playVideo(String videoId, String userType, String ipAddress) {
// 1. Authorization check embedded directly in core logic
if (!"PREMIUM".equalsIgnoreCase(userType)) {
System.out.println("Access Denied: Premium subscription required.");
return;
}
// 2. Rate limiting logic embedded in core logic
if (isRateLimited(ipAddress)) {
System.out.println("Too Many Requests: Slow down.");
return;
}
// 3. Core Business Logic (Video Streaming)
System.out.println("Fetching and streaming video data for ID: " + videoId);
}
private boolean isRateLimited(String ipAddress) {
// Primitive rate limiting check...
return false;
}
}
Architectural Deficiencies of the Naive Code
- Violation of Single Responsibility Principle (SRP):
VideoServiceis responsible for media streaming, authorization, rate limiting, and cache management simultaneously. - Code Duplication: If the system adds a
PodcastServiceorPhotoService, the exact same authorization and rate-limiting code must be copied across all services. - Tight Coupling & Poor Scalability: Modifying security policies or rate-limiting algorithms requires altering and redeploying the primary video streaming service.
3. The Core Mental Model: Proxy as a Gatekeeper
To eliminate tight coupling and protect the core domain service, introduce the Proxy Design Pattern. The proxy acts as a wall or gatekeeper positioned between the user interface and the core service.
Conceptual Flow
Separation of Concerns Matrix
4. Refactoring with the Proxy Pattern (Code Evolution)
To implement the Proxy pattern effectively, both the real service and the proxy must implement a common interface. This allows the proxy to be swapped in transparently anywhere the real service is expected.
Structural Blueprint
Step-by-Step Implementation
Step 1: Define the Shared Interface
public interface VideoService {
void playVideo(String videoId, User user);
}
Step 2: Implement the Real Subject (Core Domain)
public class RealVideoService implements VideoService {
@Override
public void playVideo(String videoId, User user) {
// Pure domain logic: Focuses exclusively on media processing
System.out.println("[RealVideoService] Rendering and streaming high-definition video ID: " + videoId);
}
}
Step 3: Implement the Proxy Subject (Access & Gatekeeping)
import java.util.HashMap;
import java.util.Map;
public class ProxyVideoService implements VideoService {
private final VideoService realVideoService;
private final Map<String, String> videoCache;
private final Map<String, Integer> requestCounts;
public ProxyVideoService(VideoService realVideoService) {
this.realVideoService = realVideoService;
this.videoCache = new HashMap<>();
this.requestCounts = new HashMap<>();
}
@Override
public void playVideo(String videoId, User user) {
System.out.println("\n[Proxy] Intercepting request for Video ID: " + videoId + " from User: " + user.getUsername());
// 1. Authorization Gatekeeping
if (!user.isPremium()) {
System.out.println("[Proxy] Access Denied: User " + user.getUsername() + " does not have a Premium subscription.");
return;
}
// 2. Rate Limiting Check
if (isRateLimited(user)) {
System.out.println("[Proxy] Rate Limit Exceeded: User " + user.getUsername() + " sent too many requests.");
return;
}
// 3. Response Caching
if (videoCache.containsKey(videoId)) {
System.out.println("[Proxy] Cache Hit: Returning cached payload for Video ID: " + videoId);
return;
}
// 4. Cache Miss: Forward request to the Real Subject
System.out.println("[Proxy] Cache Miss & Security Validated. Forwarding call to RealVideoService...");
realVideoService.playVideo(videoId, user);
// Store result in cache post-execution
videoCache.put(videoId, "VideoDataStream_" + videoId);
}
private boolean isRateLimited(User user) {
int count = requestCounts.getOrDefault(user.getUsername(), 0) + 1;
requestCounts.put(user.getUsername(), count);
return count > 3; // Allow maximum 3 requests per session for demonstration
}
}
Step 4: Supporting Models & Client Execution
public class User {
private final String username;
private final boolean isPremium;
public User(String username, boolean isPremium) {
this.username = username;
this.isPremium = isPremium;
}
public String getUsername() { return username; }
public boolean isPremium() { return isPremium; }
}
public class ClientApplication {
public static void main(String[] args) {
// Instantiate the core real service
VideoService realService = new RealVideoService();
// Wrap the real service inside the proxy
VideoService proxyService = new ProxyVideoService(realService);
User freeUser = new User("Alice", false);
User premiumUser = new User("Bob", true);
// Scenario A: Unauthorized access blocked by Proxy
proxyService.playVideo("vid_101", freeUser);
// Scenario B: Authorized access passed to Real Service
proxyService.playService("vid_101", premiumUser);
// Scenario C: Subsequent access served directly from Proxy Cache
proxyService.playVideo("vid_101", premiumUser);
}
}
5. Production Realities & Design Enhancements
In enterprise systems, bundling caching, rate limiting, and authorization into a single proxy class can lead to bloated code. To scale this architecture for production environments, combine the Proxy pattern with complementary design patterns:
1. Strategy Pattern for Rate Limiting
Instead of hardcoding rate-limiting logic inside the proxy, inject a flexible RateLimiterStrategy (e.g., Token Bucket, Sliding Window Log, Fixed Window Counter).
2. Dependency Injection (DI) & Chain of Responsibilities
For multi-layered enterprise concerns, chain multiple specialized proxies together or inject dependencies cleanly via constructors:
3. Cross-Domain Generic Proxies
Because access control logic is decoupled from business logic, the proxy architecture easily extends across modern multi-service platforms:
6. Categorization of Proxy Types
Depending on the operational problem being solved, proxies are categorized into four core variations:
7. Architectural Trade-offs & Analysis
8. Summary and Key Takeaways
- Core Purpose: The Proxy pattern places a surrogate object between clients and the real target object to control access, filter requests, or add cross-cutting behavior.
- Separation of Concerns: Core domain services (e.g.,
RealVideoService) remain purely focused on business logic ( media processing), remaining completely oblivious to rate limits, permissions, or caching mechanisms. - Structural Uniformity: The Proxy and the Real Subject must implement the exact same interface (
VideoService), ensuring the proxy can be substituted transparently without breaking client code. - Cost Optimization: By caching responses at the proxy layer, systems avoid redundant execution on expensive backend nodes, significantly reducing compute and bandwidth overhead.
- System Protection: Protection proxies act as gatekeepers, protecting downstream systems against malicious abuse, bot traffic, and unauthorized resource access.
- Extensibility: Proxy layers can be enhanced using the Strategy Pattern (to swap rate-limiting or caching algorithms dynamically) or chained sequentially to form multi-layered processing pipelines.
- Real-World Ubiquity: Beyond custom application code, proxy design principles form the foundational architecture behind CDNs (Cloudflare), Reverse Proxies (Nginx), API Gateways, and Lazy-Loading ORM frameworks (Hibernate).