Proxy
A stand-in that controls access: lazy, cached, protected or remote.
01The intuition
The problem it solves
Imagine a school library program. A Book object loads its full scanned pages from disk the moment it is created, which takes about two seconds. The catalogue screen lists 400 books. The obvious code creates 400 Book objects to show 400 titles, so the screen takes thirteen minutes to open, even though nobody has opened a single page.
class Book:
def __init__(self, title: str, path: str) -> None:
self.title = title
self.pages = load_scans(path) # 2 seconds, 50 MB
def page(self, n: int) -> bytes:
return self.pages[n]
catalogue = [Book(t, p) for t, p in shelf] # 400 x 2 s before anything shows
for b in catalogue:
print(b.title)
The painful moment is not the slowness alone. It is that every place in the program that wants a title also pays for the pages, and there is no spot where you could add a rule such as "only teachers may open page scans" or "remember pages once loaded" without editing Book itself and every caller.
Build it one piece at a time
Step one: name what callers actually need. They need something with a title and a page(n) method. Write that down as a small interface so the real book and any stand-in look identical to callers.
from typing import Protocol
class Readable(Protocol):
title: str
def page(self, n: int) -> bytes: ...
class RealBook:
def __init__(self, title: str, path: str) -> None:
self.title = title
self.pages = load_scans(path)
def page(self, n: int) -> bytes:
return self.pages[n]
Step two: build a stand-in that holds only the cheap data and remembers how to make the real thing later. It creates the real book the first time a page is asked for, and never before. Programmers call this stand-in a proxy, and this kind a lazy proxy.
class LazyBook:
def __init__(self, title: str, path: str) -> None:
self.title = title
self._path = path
self._real: RealBook | None = None
def page(self, n: int) -> bytes:
if self._real is None:
self._real = RealBook(self.title, self._path) # pay only now
return self._real.page(n)
catalogue = [LazyBook(t, p) for t, p in shelf] # instant
Step three: because the proxy has the same shape as the real book, you can stack other rules in the same place. A protecting proxy checks who is asking; a caching proxy remembers answers. Nothing in the catalogue code changes.
class GuardedBook:
def __init__(self, inner: Readable, user: str) -> None:
self.title = inner.title
self._inner = inner
self._user = user
def page(self, n: int) -> bytes:
if not is_teacher(self._user):
raise PermissionError("students may not open scans")
return self._inner.page(n)
book: Readable = GuardedBook(LazyBook("Atlas", "atlas.pdf"), user="meera")
Why it works
The principle: callers depend on the promise (Readable), not on the object that keeps it. Once that is true, you may slide any object with the same promise between the caller and the real thing. The proxy's job is to decide whether, when or how the real call happens; it does not change what the call means.
The invariant the structure keeps is that every method on the proxy eventually behaves like the same method on the real object, or fails in a way the caller expects. Where it breaks: a proxy that quietly swallows errors, returns stale cached data forever, or exposes a method the real object does not have. Interviewers test this by asking what happens when the real object fails to load, or when two threads call page at once on a lazy proxy and both try to create the real book. The honest answer is a lock or a check-after-lock around the creation.
The trade-off you must say out loud
A proxy adds one more class and one more hop per call. For an object that is cheap to build, lazy loading is pure overhead. For a cache proxy you now own an invalidation problem: when the real data changes, who clears the cache? Say this before the interviewer asks. The usual follow-up is the difference from a decorator: both wrap, but a decorator adds behaviour the caller wants (logging, retries), while a proxy controls access to something the caller thinks it is talking to directly.
Where it shows up in the 100 Google problems
- Cache manager with pluggable eviction and TTL: the cache is a caching proxy in front of the slow loader, and the eviction policy decides what the proxy forgets.
- Blocking connection pool: acquire returns a proxy around a real connection so that release can be tracked and a double release made harmless.
- Build your own Future: a future is a stand-in for a value that does not exist yet; get() blocks until the real value arrives.
- Role and permission system: a protecting proxy checks the caller's role before forwarding to the resource.