Proxy Pattern
Definitionβ
The Proxy provides a substitute or placeholder for another object to control access to it. The proxy implements the same interface as the real object and forwards requests to it, adding control logic before or after the delegation.
Coffee Shop Exampleβ
Our Coffee Shop has an expensive CoffeeMachine that takes time to initialize (heating boilers, calibrating). We use different proxies to:
- Virtual Proxy β lazy-load the expensive machine only when first needed
- Protection Proxy β restrict machine access to authorized baristas only
- Caching Proxy β cache recent brew results to avoid re-brewing identical orders
Structureβ
Subject Interfaceβ
public record Beverage(string Coffee, CupSize Size, DateTime BrewedAt);
public interface ICoffeeMachine
{
Beverage Brew(string coffee, CupSize size);
bool IsReady();
}
Real Subject β Expensive to Createβ
public class RealCoffeeMachine : ICoffeeMachine
{
private bool _initialized;
public RealCoffeeMachine()
{
// Expensive initialization β simulate 2-second startup
Console.WriteLine(" [RealCoffeeMachine] Heating boilers...");
Console.WriteLine(" [RealCoffeeMachine] Calibrating pressure...");
Console.WriteLine(" [RealCoffeeMachine] Machine ready!");
_initialized = true;
}
public Beverage Brew(string coffee, CupSize size)
{
Console.WriteLine($" [RealCoffeeMachine] Brewing {size} {coffee}...");
return new Beverage(coffee, size, DateTime.UtcNow);
}
public bool IsReady() => _initialized;
}
Virtual Proxy β Lazy Loadingβ
public class VirtualProxy : ICoffeeMachine
{
private RealCoffeeMachine? _real;
public Beverage Brew(string coffee, CupSize size)
{
// Create the real machine only when first needed
_real ??= new RealCoffeeMachine();
return _real.Brew(coffee, size);
}
public bool IsReady() => _real?.IsReady() ?? false;
}
Protection Proxy β Access Controlβ
public class ProtectionProxy : ICoffeeMachine
{
private readonly ICoffeeMachine _inner;
private readonly string _currentUser;
private readonly HashSet<string> _authorizedUsers;
public ProtectionProxy(ICoffeeMachine inner, string currentUser,
HashSet<string> authorizedUsers)
{
_inner = inner;
_currentUser = currentUser;
_authorizedUsers = authorizedUsers;
}
public Beverage Brew(string coffee, CupSize size)
{
if (!_authorizedUsers.Contains(_currentUser))
throw new UnauthorizedAccessException(
$"{_currentUser} is not authorized to use this machine.");
Console.WriteLine($" [ProtectionProxy] Access granted for {_currentUser}");
return _inner.Brew(coffee, size);
}
public bool IsReady() => _inner.IsReady();
}
Caching Proxy β Memoizationβ
public class CachingProxy : ICoffeeMachine
{
private readonly ICoffeeMachine _inner;
private readonly Dictionary<string, Beverage> _cache = new();
public CachingProxy(ICoffeeMachine inner)
{
_inner = inner;
}
public Beverage Brew(string coffee, CupSize size)
{
var key = $"{coffee}:{size}";
if (_cache.TryGetValue(key, out var cached))
{
Console.WriteLine($" [CachingProxy] Cache hit for {key}");
return cached with { BrewedAt = DateTime.UtcNow };
}
Console.WriteLine($" [CachingProxy] Cache miss for {key}");
var beverage = _inner.Brew(coffee, size);
_cache[key] = beverage;
return beverage;
}
public bool IsReady() => _inner.IsReady();
}
Client β Stacking Proxiesβ
// Create machine on demand β check access β cache results
// The client only sees ICoffeeMachine
var authorizedBaristas = new HashSet<string> { "Alice", "Bob", "Carlos" };
ICoffeeMachine machine = new CachingProxy(
new ProtectionProxy(
new VirtualProxy(),
"Alice",
authorizedBaristas));
Console.WriteLine($"Machine ready? {machine.IsReady()}");
// Machine ready? False (not created yet β lazy!)
Console.WriteLine("\n--- First order ---");
var drink1 = machine.Brew("Latte", CupSize.Large);
// [RealCoffeeMachine] Heating boilers... β created on first use
// [RealCoffeeMachine] Calibrating pressure...
// [RealCoffeeMachine] Machine ready!
// [ProtectionProxy] Access granted for Alice
// [CachingProxy] Cache miss for Latte:Large
// [RealCoffeeMachine] Brewing Large Latte...
Console.WriteLine("\n--- Same order again ---");
var drink2 = machine.Brew("Latte", CupSize.Large);
// [CachingProxy] Cache hit for Latte:Large β no brewing needed!
Console.WriteLine("\n--- Unauthorized user ---");
ICoffeeMachine unauthorizedMachine = new ProtectionProxy(
new VirtualProxy(),
"Eve",
authorizedBaristas);
try { unauthorizedMachine.Brew("Espresso", CupSize.Small); }
catch (UnauthorizedAccessException ex) { Console.WriteLine($" BLOCKED: {ex.Message}"); }
// BLOCKED: Eve is not authorized to use this machine.
Notice the stacking: CachingProxy β ProtectionProxy β VirtualProxy β RealCoffeeMachine. Each proxy adds one concern. The client sees ICoffeeMachine and doesn't know (or care) how many proxies are in the chain.
Proxy Typesβ
| Type | Purpose | Example |
|---|---|---|
| Virtual | Lazy initialization | VirtualProxy creates machine on first use |
| Protection | Access control | ProtectionProxy checks user authorization |
| Caching | Store and reuse results | CachingProxy avoids re-brewing identical orders |
| Remote | Hide network communication | gRPC client stub, WCF proxy |
| Logging | Record operations | Log every Brew() call for auditing |
| Smart | Reference counting, locking | Track usage, auto-release resources |
Proxy vs Decoratorβ
| Proxy | Decorator | |
|---|---|---|
| Intent | Control access | Add behavior |
| Creation | Proxy may create the real object | Client provides the decorated object |
| Lifecycle | Proxy manages the real object's lifecycle | Decorator doesn't manage lifecycle |
| Transparency | Client shouldn't know it's a proxy | Client intentionally stacks decorators |
.NET Real-World Usageβ
Lazy<T>β built-in virtual proxy, creates value on first access- EF Core lazy loading proxies β
ILazyLoadergenerates proxy classes that load navigation properties on access HttpClientβ proxies HTTP communication to a remote server- WCF / gRPC client stubs β remote proxy hiding network details
- ASP.NET Core
RemoteAuthenticationServiceβ proxy wrapping remote auth operations
When to Useβ
- You need to control access to an object (security, permissions)
- The real object is expensive to create and should be loaded lazily
- You want to add caching, logging, or monitoring transparently
- The real object is on a remote machine and you need a local representative
When NOT to Useβ
- Direct access to the object is fine β no access control or lazy loading needed
- The overhead of the proxy isn't justified (premature optimization)
- The proxy adds more complexity than value
Key Takeawaysβ
- Proxy controls access to another object through the same interface
- Multiple proxy types serve different purposes: virtual, protection, caching, remote
- Proxies can be stacked β each handling one concern
- The proxy may create the real object (virtual proxy) or just reference it (protection, caching)
Interview Questionsβ
Q: How is Proxy different from Decorator? Structurally they're nearly identical β both wrap an object and implement the same interface. The difference is intent: Proxy controls access (lazy loading, security), Decorator adds behavior (toppings, middleware). Proxy often manages the real object's lifecycle; Decorator doesn't.
Q: How does EF Core use proxies? EF Core can generate dynamic proxy classes at runtime that inherit from your entities. These proxies override virtual navigation properties to lazy-load related data from the database when first accessed β a classic virtual proxy.
Q: What's a remote proxy? A remote proxy hides the complexity of network communication. The client calls methods on a local object (the proxy), which serializes the call, sends it over the network, and returns the result. gRPC and WCF client stubs are remote proxies.
Related Topicsβ
- Decorator β same structure, different intent (adds behavior)
- Adapter β changes interface, Proxy keeps the same interface
- Facade β simplifies interface, Proxy doesn't simplify β it controls
- Lazy Loading in EF Core β real-world proxy usage in .NET