A proxy looks exactly like the object it stands in for. Callers can’t tell the difference. But before a request reaches the real thing, the proxy gets a say: answer from a cache, check permissions, load something lazily. CornerStore uses two proxies, stacked, and that turns out to be the most interesting part.
| Category | Structural |
| Smell fixed | S12: Inventory reads are uncached and access checks are scattered |
| Code | patterns/structural/proxy |
The Scene
Sam works in the back office. Ava is a customer. Both try to manage stock:
[047] admin.stock.adjust customer=cu-3 sku=KETTLE-1L delta=-4
stock KETTLE-1L adjusted by -4, now 0
[048] admin.stock.lookup customer=cu-3 sku=KETTLE-1L
stock KETTLE-1L 0
[049] admin.stock.adjust customer=cu-1 sku=KETTLE-1L delta=4
rejected: staff only
[050] admin.stock.lookup customer=cu-1 sku=KETTLE-1L
rejected: staff only
Staff can, customers can’t. Good. But there are two quieter problems underneath. Who’s enforcing that rule? And how many times is the code asking for the same stock level?
The Smell
First, every stock read is expensive. CornerStore simulates a slow inventory system by counting a cost for every lookup (it’s counted internally, so it never shows up in the transcript):
// SMELL S12: each uncached stock lookup adds simulated latency cost
public int GetStock(string sku)
{
lookupCost += 10;
return stock.GetValueOrDefault(sku);
}
Validation reads stock, reservation reads stock, order updates read stock. Every read pays full price, even for a SKU that was looked up a moment ago.
Second, the inventory service will let anyone change stock:
// SMELL S12: callers can change stock without an authorization check
public int AdjustStock(string sku, int adjustment)
The staff-only rule is enforced by the callers, one method at a time:
// SMELL S12: the caller checks the staff role before changing stock
if (actor.Role != "staff")
{
return "rejected: staff only";
}
That works until someone writes a new caller and forgets the check. Security that depends on every caller remembering isn’t really security.
The Idea
A Proxy implements the same interface as the real object and sits in front of it. Callers think they’re talking to the real thing. The proxy decides what actually happens: forward the call, answer it itself, or refuse.
Think of a building’s security desk. Visitors walk in the same front entrance as everyone else, but the desk checks your badge before you reach the lifts. Nobody has to trust each visitor to check themselves.
There are a few classic kinds of proxy. CornerStore uses two:
- a caching proxy, which remembers answers so the slow thing isn’t asked twice
- a protection proxy, which checks permissions before letting a call through
The Refactor
The caching proxy only goes to the real service on a cache miss:
if (!stockCache.TryGetValue(sku, out var available))
{
available = inner.GetStock(sku);
stockCache[sku] = available;
}
return available;
The protection proxy checks the role at the service boundary, before delegating:
if (actor.Role != "staff")
{
available = 0;
return false;
}
return backOfficeInventory.TryGetStaffStock(actor, sku, out available);
Then StoreConsole stacks them like layers: protection on the outside, caching inside, the real service at the core. Every caller just receives an inventory interface and has no idea how many layers are behind it.
classDiagram
class IInventoryService {
<<interface>>
+GetStock(sku)
+ReserveStock(sku, quantity)
}
class IBackOfficeInventory {
<<interface>>
}
class IStockRestorer {
<<interface>>
}
IInventoryService <|.. InventoryService
IInventoryService <|.. CachingInventoryProxy
IInventoryService <|.. ProtectionInventoryProxy
IBackOfficeInventory <|.. InventoryService
IBackOfficeInventory <|.. CachingInventoryProxy
IBackOfficeInventory <|.. ProtectionInventoryProxy
IStockRestorer <|.. InventoryService
IStockRestorer <|.. CachingInventoryProxy
IStockRestorer <|.. ProtectionInventoryProxy
CachingInventoryProxy --> IInventoryService : caches
ProtectionInventoryProxy --> IInventoryService : protects
StoreConsole --> InventoryService : creates
StoreConsole --> CachingInventoryProxy : composes inside
StoreConsole --> ProtectionInventoryProxy : composes outside
OrderValidator --> IInventoryService
BackOffice --> IBackOfficeInventory
Why protection outside caching? So that an unauthorised request is turned away before it can touch, or read from, the cache.
The hard part: keeping the cache honest
Here’s what most proxy examples skip. A cache is only useful if it’s right. The moment stock is reserved, restored or adjusted, the cached value is stale. So the caching proxy also intercepts every change and removes or refreshes the affected entry.
Get that wrong and you’ve built a very fast way to sell a kettle you don’t have. That’s why the solution’s own tests don’t just check that a second read is cached. They also check that adjusting and reserving stock refresh the cached value.
One scope note: refund and cancellation still check the staff role inside BackOffice. Those actions aren’t inventory calls, so they’re outside this seam. They get their turn in Command.
What It Cost
10 files: about 239 lines added and 26 removed. Two proxy classes, plus the inventory capabilities split into smaller interfaces so each caller gets only what it needs. The back office gets staff operations. Checkout gets reads and reservations.
When Not to Use It
- The data is shared across processes. This cache lives inside one process. If several servers updated the same stock, each would have its own stale cache. That’s a job for a distributed cache with proper invalidation, not a simple proxy.
- Callers can reach the real object anyway. A protection proxy only protects calls that go through it. If untrusted code can still get hold of the real
InventoryService, the check is decoration. - There’s nothing to control. A proxy that just forwards every call is an extra layer with no job.
Easily Confused With
Decorator. Structurally they’re nearly identical: same interface, wraps another object, forwards calls. The difference is intent. A decorator adds behaviour that the caller wants, like gift wrap adding to a price. A proxy controls access to the real thing, like caching or permission checks. See Decorator vs Proxy for the two side by side in CornerStore.
Try It Yourself
dotnet build patterns/structural/proxy/Proxy.slnx
dotnet test patterns/structural/proxy/Proxy.slnx
dotnet run --project patterns/structural/proxy/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json
Exercise: comment out the cache invalidation after a stock reservation and run the tests. CachingProxy_WhenStockIsReserved_InvalidatesCachedValue should fail. Then think about what that bug would look like in production.
The Proxy README has a sequence diagram showing both proxies in action. The full change is in DIFF.md.
That’s the Structural patterns done. Next up: the biggest group, the Behavioral patterns.
Previous: Flyweight · Next: Chain of Responsibility