Course contents

Proxy — stand-in with a purpose

Sign in to run this lab and save your progress

Learn

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.

Your task

Proxy is a stand-in for another object. It has the same interface as the real thing — but the proxy decides when (or whether) to call it.

Three flavors show up in real systems:

  1. Lazy proxy — don't build the real object until it's actually needed. (Expensive things: models, images, DB pools.)
  2. Protection proxy — check permissions before forwarding the call.
  3. Caching proxy — remember the result of expensive calls so we don't repeat them.

The problem

Your app has a DocumentStore interface:

class DocumentStore(ABC):
    @abstractmethod
    def read(self, doc_id): ...
    @abstractmethod
    def write(self, doc_id, content): ...

The real one, RemoteDocumentStore, hits a slow storage API (~200ms per read). Building it also takes ~1s of handshake at startup.

Two problems in different parts of the code:

  • Startup time: building RemoteDocumentStore right away adds 1s to app boot even if nobody reads a document. We want it lazy — only build when needed.
  • Some routes (admin) can write; most routes (public) are read-only. We want protection enforced without every route re-checking.

Both fit Proxy — same shape as the real store, controlling when/whether calls reach it.

Questions to ask about the requirements

  1. What are you controlling — construction, access, or both? Lazy (construction), protection (access), caching (repeat calls).
  2. Is the interface stable? Proxy has to match the real class's interface exactly. If the interface changes, the proxy changes too.
  3. Do callers know they're talking to a proxy? Ideally no — they hold the interface, and the proxy is invisible.
  4. How many methods on the interface? Small (1-3) → Proxy fits cleanly. Large (20+) → tedious to forward every call; look at __getattr__ tricks or a different pattern.
  5. Do you need multiple proxies at once (lazy + protection)? You can stack them the same way Decorators stack.

For our problem: lazy for construction, protection for writes, small interface, callers should be unaware. Proxy fits.

Different designs, compared

Design A — inline checks at every call site

if not user.can_write:
    raise Forbidden()
store.write(doc_id, content)
  • Good: Simple.
  • Bad: Every write site copies the check. Miss one → security bug.

Design B — checks inside RemoteDocumentStore

class RemoteDocumentStore:
    def write(self, doc_id, content, user):
        if not user.can_write: raise Forbidden()
        ...
  • Good: One place.
  • Bad: Storage class now knows about users, permissions, HTTP context. Breaks SRP. Also, you can't reuse the storage class in places that don't have users (like background jobs).

Design C — Proxy

class ProtectedDocumentStore(DocumentStore):
    def __init__(self, inner, can_write):
        self._inner = inner
        self._can_write = can_write

    def read(self, doc_id):
        return self._inner.read(doc_id)

    def write(self, doc_id, content):
        if not self._can_write:
            raise PermissionError("read-only")
        return self._inner.write(doc_id, content)
  • Good: Same interface as inner. Callers don't know. Real storage class stays out of the permission business. Stacks with other proxies (lazy + protection).
  • Bad: One extra class per proxy flavor.
  • When it wins: Any real "control access to a thing" scenario.

Proxy vs Decorator

Proxy and Decorator look identical in structure — both wrap an object with the same interface. Different intent:

  • Decorator adds NEW behavior around the wrapped object (logging, retries, extra features).
  • Proxy controls ACCESS to the wrapped object (lazy, protection, caching, remote).

In practice the line is blurry. If you're adding features callers should see → Decorator. If you're quietly controlling access → Proxy. Interviewers care that you can tell them apart; they don't care where the line is drawn on tricky edge cases.

The locked-in choice: Design C

Reviewing the requirements:

  • Storage class must stay out of the permission business. → Rules out B.
  • Many write sites; forgetting the check is a security bug. → Rules out A.
  • Stacks with a lazy proxy. → C naturally stacks.

Design C wins.

Mechanics

from abc import ABC, abstractmethod


class DocumentStore(ABC):
    @abstractmethod
    def read(self, doc_id): ...
    @abstractmethod
    def write(self, doc_id, content): ...


class RemoteDocumentStore(DocumentStore):
    def __init__(self):
        # simulate expensive init
        self._data = {}
        self.init_count = 1  # to prove construction happened

    def read(self, doc_id):
        return self._data.get(doc_id)

    def write(self, doc_id, content):
        self._data[doc_id] = content


class LazyDocumentStoreProxy(DocumentStore):
    """Constructs the real store on first use."""

    def __init__(self, factory):
        self._factory = factory   # zero-arg callable
        self._real = None

    def _get_real(self):
        if self._real is None:
            self._real = self._factory()
        return self._real

    def read(self, doc_id):
        return self._get_real().read(doc_id)

    def write(self, doc_id, content):
        return self._get_real().write(doc_id, content)


class ProtectedDocumentStoreProxy(DocumentStore):
    """Enforces read-only for unauthorized callers."""

    def __init__(self, inner, can_write):
        self._inner = inner
        self._can_write = can_write

    def read(self, doc_id):
        return self._inner.read(doc_id)

    def write(self, doc_id, content):
        if not self._can_write:
            raise PermissionError("read-only")
        return self._inner.write(doc_id, content)

Your task

In main.py build:

  1. Abstract DocumentStore(ABC) with read(doc_id) and write(doc_id, content).
  2. RemoteDocumentStore(DocumentStore):
    • __init__ initializes self._data = {} and self.init_count = 1 (a marker so tests can verify construction happened).
    • read(doc_id) returns self._data.get(doc_id)
    • write(doc_id, content) stores in _data
  3. LazyDocumentStoreProxy(DocumentStore):
    • __init__(self, factory) stores a zero-arg factory callable, self._real = None.
    • Constructs the real store on FIRST access (either read or write).
    • Subsequent accesses reuse the same real instance.
    • Never constructs if no access happens.
  4. ProtectedDocumentStoreProxy(DocumentStore):
    • __init__(self, inner, can_write) stores both
    • read delegates
    • write raises PermissionError("read-only") if can_write is False, otherwise delegates

Structural tests enforce:

  • Both proxies IS-A DocumentStore.
  • Lazy proxy doesn't construct real store on init.
  • Lazy proxy constructs exactly ONCE across many accesses.
  • Protection proxy allows reads, blocks unauthorized writes.
  • Proxies stack (Protected(Lazy(...))): lazy still lazy, protection still enforced.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
09-proxy-starter