Before you start
Proxy is a small pattern with lots of flavors. Learn the three that matter and you're 95% there:
- Lazy proxy — build the real thing on first use
- Protection proxy — check permissions before forwarding
- Caching proxy — remember results, skip real calls when possible
Each has a distinct real-world use case. Each looks the same in code: hold an inner object, forward calls, add some control in between.
The one-question test: Proxy vs Decorator
Both wrap. Both share the interface. Both stack. Different intent:
- Decorator adds a new BEHAVIOR that callers should notice. Log lines. Retries. Extra output.
- Proxy CONTROLS access to the wrapped object. Callers should NOT notice — the point is that the proxy makes the system faster, safer, or cheaper without changing what callers see.
When in doubt, ask: what's the caller supposed to see? "No difference from talking to the real thing" → Proxy. "New side effects or transformed output" → Decorator.
Real code blurs this. Interviewers accept "Proxy or Decorator, here's my reasoning" as an answer.
Where Proxy shows up
- ORM lazy relationships. SQLAlchemy and Django use proxies for related-object access so the database query only fires when you actually touch the field.
- Image / model loading. Load large assets on demand.
- API rate-limiting middleware. A proxy in front of a real service that throttles.
- File permission systems. Read/write access enforced by a proxy in front of storage.
- DNS / CDN caching. A proxy that caches responses.
- Auth guards in Django / Nest. A proxy that raises Unauthorized before your real handler runs.
- Remote-object proxies (Java RMI, gRPC, XML-RPC clients). A local proxy stands in for a remote service — every call actually hits the network. Callers hold a "local" object and can't tell.
Different flavor, same structure.
The lazy pattern subtlety
Lazy proxies work great UNTIL:
- The real object's
__init__has side effects the caller expects at proxy-build time. (Rare, but exists — like registering itself with a global registry.) Fix: build eagerly, but still proxy for other reasons. - Testing: the factory runs at first use, not at test setup. Tests that expect the real object to exist at fixture time can be surprised.
- Thread safety: two threads hitting the proxy at once
might build the real object twice. Fix: put a lock
around
_get_real().
Our lesson skips these. Just know they exist.
Protection = the security pattern you didn't know had a name
Every framework's auth system is basically proxies.
Django's @login_required. NestJS's @UseGuards().
FastAPI's Depends(get_current_user). All variations
on the Proxy pattern's protection flavor.
When you see "auth runs before the real handler, without the handler knowing" — that's Proxy.
The interview trap
"Would you use Proxy for X?" — interviewers often want to see you tell these apart:
- Proxy vs Decorator (control vs new behavior)
- Proxy vs Adapter (same interface vs shape translation)
- Proxy vs Facade (single object vs multi-service coordinator)
Say the difference out loud. "It's Proxy because I'm controlling access, not adding new behavior." That one sentence gets you full credit.
The decision cheat-sheet
| Situation | Pattern |
|---|---|
| Real object slow to build, might not be used | Lazy Proxy |
| Real object is remote / slow to call each time | Caching Proxy |
| Callers need permission checks around a real object | Protection Proxy |
| Real object lives on another machine | Remote Proxy (gRPC etc.) |
| Add NEW behavior around calls | Decorator (last lesson) |
| Translate mismatched interfaces | Adapter |
| Coordinate multiple services behind one entry | Facade |
Multiple proxies stack
ProtectedDocumentStoreProxy(LazyDocumentStoreProxy(factory), can_write=True)
gives you both. Same stacking mechanic as Decorators.
Order matters: the OUTER proxy runs its logic first
and forwards to the inner. So Protected(Lazy(...))
checks permission BEFORE lazy-building the real store.
The reverse would build the store on every write
attempt including denied ones — usually not what you
want.