Proxy Design Pattern Tutorial from Scratch (2026)
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Proxies are used for lazy loading (virtual proxy), access control (protection proxy), remote communication (remote proxy), caching (cache proxy), and logging (logging proxy). The proxy implements the same interface as the real subject, so it is transparent to the client.
This tutorial covers virtual proxies for lazy loading of expensive resources, protection proxies for access control, remote proxies for distributed systems, and cache proxies to improve performance. All examples include real Java and C++ code.
Virtual Proxy: Lazy Loading
A virtual proxy delays the creation of an expensive object until it is actually needed. This is particularly useful for loading large images, heavy documents, or database records that may not be accessed immediately. The proxy stands in for the real object and performs lightweight initialization, deferring the heavy lifting to when the client actually requests the resource.
interface Image {
void display();
Dimension getDimension();
}
class HighResolutionImage implements Image {
private String filePath;
private byte[] pixelData;
public HighResolutionImage(String filePath) {
this.filePath = filePath;
loadFromDisk(); // Expensive operation
}
private void loadFromDisk() {
System.out.println("Loading high-res image from " + filePath + "...");
try { Thread.sleep(2000); } catch (InterruptedException e) { }
System.out.println("Image loaded.");
}
public void display() {
System.out.println("Displaying image: " + filePath);
}
public Dimension getDimension() { return new Dimension(1920, 1080); }
}
// Virtual Proxy
class ImageProxy implements Image {
private String filePath;
private HighResolutionImage realImage;
private Dimension cachedDimension;
public ImageProxy(String filePath) {
this.filePath = filePath;
}
public void display() {
if (realImage == null) {
realImage = new HighResolutionImage(filePath); // Lazy load
}
realImage.display();
}
public Dimension getDimension() {
if (cachedDimension == null) {
cachedDimension = getImageDimensionFromMetadata(filePath);
}
return cachedDimension;
}
}
// Instead of loading all images upfront
List gallery = List.of(
new ImageProxy("photo1.jpg"),
new ImageProxy("photo2.jpg"),
new ImageProxy("photo3.jpg")
);
// No heavy loading yet!
// Only loads when display() is called
gallery.get(0).display();
Protection Proxy: Access Control
A protection proxy controls access to an object based on permissions or roles. The proxy checks the caller's credentials before delegating requests to the real subject. This separates access control logic from business logic, keeping the real subject focused on its core responsibility.
interface BankAccount {
void deposit(double amount);
void withdraw(double amount);
double getBalance();
}
class RealBankAccount implements BankAccount {
private double balance;
public void deposit(double amount) {
balance += amount;
System.out.println("Deposited: $" + amount);
}
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrawn: $" + amount);
} else {
throw new RuntimeException("Insufficient funds");
}
}
public double getBalance() { return balance; }
}
// Protection Proxy
class AccountProtectionProxy implements BankAccount {
private RealBankAccount account;
private User user;
public AccountProtectionProxy(User user) {
this.user = user;
this.account = new RealBankAccount();
}
public void deposit(double amount) {
account.deposit(amount);
}
public void withdraw(double amount) {
if (!user.hasPermission(Permission.WITHDRAW)) {
throw new SecurityException("Access denied: cannot withdraw");
}
account.withdraw(amount);
}
public double getBalance() {
if (!user.hasPermission(Permission.VIEW_BALANCE)) {
throw new SecurityException("Access denied: cannot view balance");
}
return account.getBalance();
}
}
// Usage
User admin = new User("admin", Set.of(Permission.WITHDRAW, Permission.VIEW_BALANCE));
BankAccount account = new AccountProtectionProxy(admin);
account.deposit(1000);
account.withdraw(100);
System.out.println("Balance: $" + account.getBalance());
Cache Proxy in C++
A cache proxy stores the results of expensive operations and returns cached results for repeated requests. This improves performance significantly for operations like database queries, API calls, or computationally intensive calculations. The cache proxy checks the cache first and only delegates to the real subject on a cache miss.
class WeatherService {
public:
virtual double getTemperature(const std::string& city) = 0;
virtual ~WeatherService() = default;
};
class RealWeatherService : public WeatherService {
public:
double getTemperature(const std::string& city) override {
// Simulate expensive API call
std::this_thread::sleep_for(std::chrono::seconds(2));
double temp = 20.0 + (std::hash{}(city) % 15);
std::cout << "Fetched temperature for " << city << ": " << temp << "\n";
return temp;
}
};
class CachedWeatherService : public WeatherService {
std::unique_ptr realService;
std::unordered_map cache;
std::mutex cacheMutex;
const std::chrono::minutes ttl{10};
struct CacheEntry {
double temperature;
std::chrono::steady_clock::time_point timestamp;
};
public:
CachedWeatherService()
: realService(std::make_unique()) {}
double getTemperature(const std::string& city) override {
std::lock_guard lock(cacheMutex);
auto it = cache.find(city);
if (it != cache.end()) {
auto age = std::chrono::steady_clock::now() - it->second.timestamp;
if (age < ttl) {
std::cout << "Cache hit for " << city << "\n";
return it->second.temperature;
}
}
double temp = realService->getTemperature(city);
cache[city] = {temp, std::chrono::steady_clock::now()};
return temp;
}
};
// Usage
CachedWeatherService weather;
std::cout << weather.getTemperature("London") << "\n"; // API call
std::cout << weather.getTemperature("London") << "\n"; // Cache hit
Frequently Asked Questions
What are the different types of proxies?
Common proxy types include: Virtual Proxy (lazy loading), Protection Proxy (access control), Remote Proxy (local representative for remote objects), Cache Proxy (caching results), Logging Proxy (audit trail), and Smart Reference Proxy (reference counting, thread safety).
How is Proxy different from Decorator?
Both have the same structure (wrapping the same interface), but their intent differs. Decorator adds new behavior to enhance functionality. Proxy controls access to the object, often for non-functional concerns like lazy loading, caching, or access control.
When should I use a Virtual Proxy?
Use a Virtual Proxy when creating the real object is expensive and may not always be needed. Examples include loading large images in a gallery app, instantiating heavyweight database connections, or loading complex documents that the user may never open.
Can Proxy be combined with other patterns?
Yes, Proxy works well with Factory (to create proxy transparently), Singleton (for proxy instance management), and Observer (to notify when real subject state changes). Dynamic proxies in Java (java.lang.reflect.Proxy) let you create proxies at runtime for any interface.
Originally published on Ayodhyyya. Last updated June 1, 2026.