- Shift Elevate
- Posts
- Proxy Pattern: Control Access to Objects with Smart Intermediaries
Proxy Pattern: Control Access to Objects with Smart Intermediaries
The Pain of Loading Expensive Resources
Picture this: You're building a photo gallery application that displays high-resolution images. Your initial approach seems logical: load all images when the gallery opens so users can browse smoothly.
Then your beta testers start complaining. The app takes forever to load. Users on mobile devices watch their data allowance disappear. Some users experience crashes on older devices. You check the analytics and discover users typically view only a handful of images before leaving, yet you're eagerly loading every single high-resolution image in the entire gallery.
You try to optimize by reducing image quality, but the product team pushes back: "Our users expect crisp, high-quality photos. That's our competitive advantage." You're stuck between loading everything upfront or compromising on quality.
The Proxy pattern solves this by providing a lightweight stand-in for expensive objects, controlling when and how they're actually loaded.
Understanding the Proxy Pattern
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. The proxy acts as an intermediary, presenting the same interface as the real object while adding functionality like lazy loading, access control, caching, or logging.
Think of it like viewing images on a modern photo sharing app: instead of loading every full-resolution image immediately (which would be slow), the app shows lightweight thumbnails first. Only when you tap an image does it load the full resolution version. The thumbnail is a proxy for the real high-resolution image.
This pattern promotes Resource Efficiency, Controlled Access, and Performance Optimization while maintaining the same interface as the real object.

Proxy Pattern Components
Core Components
Subject Interface: Defines the common interface for both RealSubject and Proxy.
RealSubject: The real object that the proxy represents (expensive to create/access).
Proxy: Maintains a reference to RealSubject and controls access to it.
Subject Interface
public interface Image {
void display();
void loadFullResolution();
ImageMetadata getMetadata();
}Supporting Classes
public class ImageMetadata {
private String filename;
private String format;
private long fileSize;
public ImageMetadata(String filename, String format, long fileSize) {
this.filename = filename;
this.format = format;
this.fileSize = fileSize;
}
public String getFilename() { return filename; }
public String getFormat() { return format; }
public long getFileSize() { return fileSize; }
@Override
public String toString() {
return String.format("%s [%s] - %.2f MB",
filename, format, fileSize / (1024.0 * 1024.0));
}
}
public class ThumbnailImage {
private String filename;
private byte[] thumbnailData;
public ThumbnailImage(String filename, ImageMetadata metadata) {
this.filename = filename;
loadThumbnail();
}
private void loadThumbnail() {
System.out.println("Loading thumbnail for: " + filename);
this.thumbnailData = new byte[102400];
System.out.println("Thumbnail loaded: " + filename + " (100 KB)");
}
public void display() {
System.out.println("Displaying thumbnail: " + filename);
}
}RealSubject
public class HighResolutionImage implements Image {
private byte[] imageData;
private ImageMetadata metadata;
private boolean isLoaded;
public HighResolutionImage(ImageMetadata metadata) {
this.metadata = metadata;
this.isLoaded = false;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading high-resolution image: " + metadata.getFilename());
long startTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
this.imageData = new byte[(int) metadata.getFileSize()];
this.isLoaded = true;
long loadTime = System.currentTimeMillis() - startTime;
System.out.println(String.format("High-res image loaded: %s (%.2f MB) in %d ms",
metadata.getFilename(), metadata.getFileSize() / (1024.0 * 1024.0), loadTime));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void display() {
if (isLoaded) {
System.out.println("Displaying high-resolution image: " + metadata.getFilename());
} else {
System.out.println("Image not loaded yet: " + metadata.getFilename());
}
}
@Override
public void loadFullResolution() {
if (!isLoaded) {
loadFromDisk();
}
}
@Override
public ImageMetadata getMetadata() {
return metadata;
}
}Proxy
public class ImageProxy implements Image {
private HighResolutionImage realImage;
private ThumbnailImage thumbnail;
private ImageMetadata metadata;
private boolean isFullResolutionLoaded;
public ImageProxy(ImageMetadata metadata) {
this.metadata = metadata;
this.isFullResolutionLoaded = false;
this.thumbnail = new ThumbnailImage(metadata.getFilename(), metadata);
}
@Override
public void display() {
if (!isFullResolutionLoaded) {
System.out.println("Displaying thumbnail preview for: " + metadata.getFilename());
thumbnail.display();
} else {
realImage.display();
}
}
@Override
public void loadFullResolution() {
if (realImage == null) {
System.out.println("User requested full resolution for: " + metadata.getFilename());
realImage = new HighResolutionImage(metadata);
isFullResolutionLoaded = true;
} else {
System.out.println("Full resolution already loaded for: " + metadata.getFilename());
}
}
@Override
public ImageMetadata getMetadata() {
return metadata;
}
public boolean isFullResolutionLoaded() {
return isFullResolutionLoaded;
}
}Client
public class PhotoGallery {
private List<Image> images;
private String galleryName;
public PhotoGallery(String galleryName) {
this.galleryName = galleryName;
this.images = new ArrayList<>();
}
public void addImage(Image image) {
images.add(image);
}
public void displayGallery() {
System.out.println("\n=== " + galleryName + " ===");
for (int i = 0; i < images.size(); i++) {
System.out.println("Image " + (i + 1) + ":");
images.get(i).display();
}
}
public void viewImage(int index) {
System.out.println("\n=== Viewing Image " + (index + 1) + " ===");
Image image = images.get(index);
System.out.println("Metadata: " + image.getMetadata().toString());
image.loadFullResolution();
image.display();
}
public void showStatistics() {
int loadedCount = 0;
for (Image image : images) {
if (image instanceof ImageProxy && ((ImageProxy) image).isFullResolutionLoaded()) {
loadedCount++;
}
}
System.out.println("\n=== Statistics ===");
System.out.println("Total: " + images.size() + " | Loaded: " + loadedCount +
" | Memory saved: " + (images.size() - loadedCount) * 100 / images.size() + "%");
}
}
public class ImageGalleryDemo {
public static void main(String[] args) {
System.out.println("=== Photo Gallery with Proxy Pattern ===\n");
PhotoGallery gallery = new PhotoGallery("Vacation Photos");
gallery.addImage(new ImageProxy(
new ImageMetadata("sunset_beach.jpg", "JPEG", 10485760)));
gallery.addImage(new ImageProxy(
new ImageMetadata("mountain_peak.jpg", "JPEG", 12582912)));
gallery.addImage(new ImageProxy(
new ImageMetadata("city_skyline.jpg", "JPEG", 11534336)));
System.out.println("\nGallery loaded! Only thumbnails in memory.\n");
gallery.displayGallery();
gallery.viewImage(1);
gallery.viewImage(1);
gallery.showStatistics();
}
}Expected Output:
=== Photo Gallery with Proxy Pattern ===
Loading thumbnail for: sunset_beach.jpg
Thumbnail loaded: sunset_beach.jpg (100 KB)
Loading thumbnail for: mountain_peak.jpg
Thumbnail loaded: mountain_peak.jpg (100 KB)
Loading thumbnail for: city_skyline.jpg
Thumbnail loaded: city_skyline.jpg (100 KB)
Gallery loaded! Only thumbnails in memory.
=== Vacation Photos ===
Image 1:
Displaying thumbnail preview for: sunset_beach.jpg
Displaying thumbnail: sunset_beach.jpg
Image 2:
Displaying thumbnail preview for: mountain_peak.jpg
Displaying thumbnail: mountain_peak.jpg
Image 3:
Displaying thumbnail preview for: city_skyline.jpg
Displaying thumbnail: city_skyline.jpg
=== Viewing Image 2 ===
Metadata: mountain_peak.jpg [JPEG] - 12.00 MB
User requested full resolution for: mountain_peak.jpg
Loading high-resolution image: mountain_peak.jpg
High-res image loaded: mountain_peak.jpg (12.00 MB) in 2001 ms
Displaying high-resolution image: mountain_peak.jpg
=== Viewing Image 2 ===
Metadata: mountain_peak.jpg [JPEG] - 12.00 MB
Full resolution already loaded for: mountain_peak.jpg
Displaying high-resolution image: mountain_peak.jpg
=== Statistics ===
Total: 3 | Loaded: 1 | Memory saved: 66%🚀 Get the Complete Implementation
The full code with advanced proxy implementations is available in our Design Patterns Repository.
# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=ProxyPatternTestReal World Examples
The Proxy pattern is extensively used in real world applications:
1. Database Lazy Loading
Object-Relational Mapping frameworks use proxy objects for lazy loading of database entities. When you fetch a parent entity, related child entities are represented by proxy objects that only load from the database when actually accessed, avoiding the N+1 query problem and improving performance.
2. Aspect-Oriented Programming
Modern application frameworks use dynamic proxies to add cross-cutting concerns like transaction management, security, and logging to business objects. The proxy intercepts method calls and adds behavior before or after the actual method execution without modifying the original code.
3. Content Delivery Networks
Content delivery networks act as caching proxies for web content, storing copies of images, videos, and static files closer to users geographically. When a user requests content, the proxy serves the cached version if available, only fetching from the origin server when necessary, dramatically reducing latency and bandwidth costs.
When to Use the Proxy Pattern
Understanding when to apply the Proxy pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:
✅ Ideal Scenarios:
You need to delay expensive object creation until actually needed.
You want to control access to an object based on permissions.
You need to represent remote objects locally.
You want to cache results of expensive operations.
You need to add reference counting or resource management.
❌ Skip It When:
The object being proxied is already lightweight and fast to create.
The added indirection and complexity outweigh the benefits.
You need direct access to the object's internal state.
The proxy layer would add unnecessary performance overhead for your use case.
Next Steps: Apply Proxy Pattern in Your Project
Ready to implement the Proxy pattern in your own projects? Here's a structured approach to get you started:
Identify Expensive Operations: Look for areas where object creation, resource loading, or method calls are costly in terms of time, memory, or network bandwidth.
Define Common Interface: Create an interface that both the real object and proxy will implement.
Implement Real Subject: Build the actual implementation with the expensive or protected functionality.
Create Proxy Class: Implement a proxy that controls access to the real subject.
Add Proxy Logic: Implement lazy loading, caching, access control, or logging as needed.
Test Performance Impact: Measure the actual benefits in terms of load time, memory usage, and user experience.
The Proxy pattern transforms resource-intensive applications into efficient, responsive systems. By controlling when and how expensive objects are created and accessed, you build applications that start faster, use less memory, and provide better user experiences while maintaining clean, maintainable code.
Found this helpful? Share it with a colleague who's struggling with slow-loading resources or memory-intensive applications. Got questions? We'd love to hear from you at [email protected]