A stand-in with the same interface as the real object. The caller can't tell it is there, and the stand-in decides when, whether and how the real object is reached.
Open a product page in a shopping app. It has five photos in a carousel, and almost everyone looks at the first one. It also shows a price, which comes from a price service across the network, and the page asks again every time it redraws. If the page loads all five photos when it opens, and calls the service on every redraw, it pays for work nobody wanted.
The Proxy pattern is the fix: put an object in front of the real one that has the same interface, so the page doesn't change, and let the stand-in decide when the real work happens. A lazy photo loads on the first look. A caching price asks the service once. This lesson builds both in Python, Java, C++ and JavaScript, and then does the part that decides whether you can explain it: the kinds of proxy, how each language lets you build one, and the difference between a proxy, a decorator and an adapter. One case study has a proxy in its code, the key-value store, and one has the word "proxy" without the pattern, the auction. We say which is which.
You ask an office front desk for what you would ask Meera, in the same words. The desk decides when she is reached, and you never learn how.
The first version: the page pays for everything#
The first product page loads its photos in the constructor and asks the price service whenever it renders:
# first_version.py: a product page that loads every photo up front and asks the price service on every render
class PhotoStore:
def __init__(self) -> None:
self.loads = 0
def load(self, file: str) -> str: # slow: read the file and decode it
self.loads += 1
return f"pixels of {file}"
class PriceService:
def __init__(self, prices) -> None:
self.prices, self.calls = dict(prices), 0
def price(self, sku: str) -> int: # slow: a trip over the network
self.calls += 1
return self.prices[sku]
class ProductPage:
def __init__(self, store, prices, sku, files) -> None:
self.photos = [store.load(f) for f in files] # all of them, even if nobody scrolls
self.prices, self.sku = prices, sku
def render(self) -> str:
return f"{self.prices.price(self.sku)} rupees, photo 1 of {len(self.photos)}" # asks again every time
store, service = PhotoStore(), PriceService({"p-1": 300})
page = ProductPage(store, service, "p-1", [f"p{i}.jpg" for i in range(1, 6)])
print(f"page opened: {store.loads} photos loaded")
for n in range(1, 5):
shown = page.render()
if n in (1, 4):
print(f"render {n}: {shown}")
print(f"price service calls: {service.calls}")// FirstVersion.java: a product page that loads every photo up front and asks the price service on every render
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class FirstVersion {
static final class PhotoStore {
int loads = 0;
String load(String file) { // slow: read the file and decode it
loads++;
return "pixels of " + file;
}
}
static final class PriceService {
private final Map<String, Integer> prices;
int calls = 0;
PriceService(Map<String, Integer> prices) { this.prices = prices; }
int price(String sku) { // slow: a trip over the network
calls++;
return prices.get(sku);
}
}
static final class ProductPage {
private final List<String> photos = new ArrayList<>();
private final PriceService prices;
private final String sku;
ProductPage(PhotoStore store, PriceService prices, String sku, List<String> files) {
for (String f : files) photos.add(store.load(f)); // all of them, even if nobody scrolls
this.prices = prices;
this.sku = sku;
}
String render() {
return prices.price(sku) + " rupees, photo 1 of " + photos.size(); // asks again every time
}
}
public static void main(String[] args) {
PhotoStore store = new PhotoStore();
PriceService service = new PriceService(Map.of("p-1", 300));
ProductPage page = new ProductPage(store, service, "p-1", List.of("p1.jpg", "p2.jpg", "p3.jpg", "p4.jpg", "p5.jpg"));
System.out.println("page opened: " + store.loads + " photos loaded");
for (int n = 1; n <= 4; n++) {
String shown = page.render();
if (n == 1 || n == 4) System.out.println("render " + n + ": " + shown);
}
System.out.println("price service calls: " + service.calls);
}
}// first_version.cpp: a product page that loads every photo up front and asks the price service on every render
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>
struct PhotoStore {
int loads = 0;
std::string load(const std::string& file) { // slow: read the file and decode it
++loads;
return "pixels of " + file;
}
};
class PriceService {
std::map<std::string, int> prices_;
public:
int calls = 0;
explicit PriceService(std::map<std::string, int> prices) : prices_(std::move(prices)) {}
int price(const std::string& sku) { // slow: a trip over the network
++calls;
return prices_.at(sku);
}
};
class ProductPage {
std::vector<std::string> photos_;
PriceService& prices_;
std::string sku_;
public:
ProductPage(PhotoStore& store, PriceService& prices, std::string sku, const std::vector<std::string>& files) : prices_(prices), sku_(std::move(sku)) {
for (const auto& f : files) photos_.push_back(store.load(f)); // all of them, even if nobody scrolls
}
std::string render() {
return std::to_string(prices_.price(sku_)) + " rupees, photo 1 of " + std::to_string(photos_.size()); // asks again every time
}
};
int main() {
PhotoStore store;
PriceService service({{"p-1", 300}});
ProductPage page(store, service, "p-1", {"p1.jpg", "p2.jpg", "p3.jpg", "p4.jpg", "p5.jpg"});
std::cout << "page opened: " << store.loads << " photos loaded\n";
for (int n = 1; n <= 4; ++n) {
std::string shown = page.render();
if (n == 1 || n == 4) std::cout << "render " << n << ": " << shown << "\n";
}
std::cout << "price service calls: " << service.calls << "\n";
}// firstVersion.js: a product page that loads every photo up front and asks the price service on every render
class PhotoStore {
constructor() {
this.loads = 0;
}
load(file) { // slow: read the file and decode it
this.loads++;
return `pixels of ${file}`;
}
}
class PriceService {
constructor(prices) {
this.prices = { ...prices };
this.calls = 0;
}
price(sku) { // slow: a trip over the network
this.calls++;
return this.prices[sku];
}
}
class ProductPage {
constructor(store, prices, sku, files) {
this.photos = files.map((f) => store.load(f)); // all of them, even if nobody scrolls
this.prices = prices;
this.sku = sku;
}
render() {
return `${this.prices.price(this.sku)} rupees, photo 1 of ${this.photos.length}`; // asks again every time
}
}
const store = new PhotoStore();
const service = new PriceService({ "p-1": 300 });
const page = new ProductPage(store, service, "p-1", [1, 2, 3, 4, 5].map((i) => `p${i}.jpg`));
console.log(`page opened: ${store.loads} photos loaded`);
for (let n = 1; n <= 4; n++) {
const shown = page.render();
if (n === 1 || n === 4) console.log(`render ${n}: ${shown}`);
}
console.log(`price service calls: ${service.calls}`);page opened: 5 photos loaded
render 1: 300 rupees, photo 1 of 5
render 4: 300 rupees, photo 1 of 5
price service calls: 4Count it by hand. The page opens with five photos in its list, so five loads, and the shopper looks at the first one. The page then renders four times (the shopper taps the heart, expands a section, rotates the phone), and each render asks the price service for the same 300 rupees: 4 calls for one fact. Both costs are wasted work, one at start-up and one on every redraw.
- Start-up pays for what isn't used. Five loads now, fifty when the catalogue shows every angle, and the page is slow to open on a poor connection.
- Every redraw repeats a network trip. The price hasn't changed, and the page asks again.
- The page knows too much. It decides how photos load and how often to ask for a price, which are not the page's job.
- Fixing it in the page spreads. A cache inside
ProductPageis a cache that the category page, the cart and the search results each write again.
Before the refactor: what stays the same?
You want the page to load photos later and ask for the price less often, without editing the page. What has to stay the same for that to work?
One strong answer
The interface the page uses. If the stand-in has the same methods as the real photo and the real price service, the page can't tell them apart, so the page stays as it is and the stand-in decides when to do the real work. That is a proxy: the same interface, and control over access.
The pattern#
The book that named it (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states the intent of Proxy as: "Provide a surrogate or placeholder for another object to control access to it."
| Role | In the product page | Its one job |
|---|---|---|
| Subject | Photo, PriceService |
The interface that the client uses and that both the real object and the proxy implement |
| Real subject | RealPhoto, SlowPrices |
Does the actual work, and is the slow or restricted part |
| Proxy | LazyPhoto, CachingPrices |
Implements the subject's interface, holds (or will create) the real subject, and decides when and whether to call it |
| Client | The product page | Uses the subject interface, and can't tell a proxy from the real thing |
%% title: Proxy: the client sees only the subject interface, and the proxy decides when the real one is reached
classDiagram
class Photo {
<<interface>>
+show() str
}
class RealPhoto
class LazyPhoto
class PriceService {
<<interface>>
+price(sku) int
}
class SlowPrices
class CachingPrices
class ProductPage
Photo <|.. RealPhoto : implements
Photo <|.. LazyPhoto : implements
LazyPhoto --> RealPhoto : creates on first show
PriceService <|.. SlowPrices : implements
PriceService <|.. CachingPrices : implements
CachingPrices --> SlowPrices : asks once per sku
ProductPage --> Photo : show
ProductPage --> PriceService : priceRead the arrows. The page has two arrows, both to interfaces, and none to a proxy or a real class. Each proxy implements the same interface as the real object (the dotted arrows) and holds a reference to it (the solid arrows to RealPhoto and SlowPrices). That pair, same interface and a reference to the real one, is the shape of every proxy. What the proxy does between receiving a call and forwarding it is what makes it a lazy, caching, protection or remote proxy.
The page uses only the Photo interface. LazyPhoto and RealPhoto both implement it, and LazyPhoto creates the real photo on the first show, so the page never knows.
The code: a lazy photo and a caching price#
Here are both proxies in all four languages. LazyPhoto builds the RealPhoto (whose constructor does the loading) on the first show(). CachingPrices asks the real service once per product code, and remembers the answer. Neither class is known to the page, which holds a Photo and a PriceService:
# proxies.py: a lazy photo and a caching price service, each with the same interface as the real thing
from abc import ABC, abstractmethod
class Photo(ABC): # the subject's interface: all the page ever uses
@abstractmethod
def show(self) -> str: ...
class RealPhoto(Photo): # the real subject: loading happens in the constructor, which is the slow part
def __init__(self, file: str, log: list) -> None:
log.append(f"load {file}")
self.file = file
def show(self) -> str:
return f"showing {self.file}"
class LazyPhoto(Photo): # a virtual proxy: builds the real photo on the first show()
def __init__(self, file: str, log: list) -> None:
self.file, self.log, self.real = file, log, None
def show(self) -> str:
if self.real is None:
self.real = RealPhoto(self.file, self.log)
return self.real.show()
class PriceService(ABC):
@abstractmethod
def price(self, sku: str) -> int: ...
class SlowPrices(PriceService): # the real subject: every call is a trip over the network
def __init__(self, table) -> None:
self.table, self.calls = dict(table), 0
def price(self, sku: str) -> int:
self.calls += 1
return self.table[sku]
def change(self, sku: str, amount: int) -> None: # the shop edits a price
self.table[sku] = amount
class CachingPrices(PriceService): # a caching proxy: asks the real service once per sku
def __init__(self, real: PriceService) -> None:
self.real, self.seen = real, {}
def price(self, sku: str) -> int:
if sku not in self.seen:
self.seen[sku] = self.real.price(sku)
return self.seen[sku]
def invalidate(self, sku: str) -> None: # a cache that is never told to forget will be wrong
self.seen.pop(sku, None)
log = []
photos = [LazyPhoto(f"p{i}.jpg", log) for i in range(1, 6)]
print(f"page opened with {len(photos)} photos, loads so far: {len(log)}")
print(f"photo 1: {photos[0].show()}")
print(f"photo 1 again: {photos[0].show()}")
print(f"loads in total: {len(log)} ({log[0]})")
slow = SlowPrices({"p-1": 300})
prices = CachingPrices(slow)
asked = [prices.price("p-1") for _ in range(4)]
print(f"p-1 asked 4 times: {' '.join(str(a) for a in asked)}, service calls: {slow.calls}")
slow.change("p-1", 320)
print(f"shop changes p-1 to 320, the cache says {prices.price('p-1')}")
prices.invalidate("p-1")
print(f"after invalidate: {prices.price('p-1')}, service calls: {slow.calls}")// Proxies.java: a lazy photo and a caching price service, each with the same interface as the real thing
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Proxies {
interface Photo { // the subject's interface: all the page ever uses
String show();
}
static final class RealPhoto implements Photo { // the real subject: loading happens in the constructor, which is the slow part
private final String file;
RealPhoto(String file, List<String> log) {
log.add("load " + file);
this.file = file;
}
public String show() { return "showing " + file; }
}
static final class LazyPhoto implements Photo { // a virtual proxy: builds the real photo on the first show()
private final String file;
private final List<String> log;
private Photo real = null;
LazyPhoto(String file, List<String> log) {
this.file = file;
this.log = log;
}
public String show() {
if (real == null) real = new RealPhoto(file, log);
return real.show();
}
}
interface PriceService {
int price(String sku);
}
static final class SlowPrices implements PriceService { // the real subject: every call is a trip over the network
private final Map<String, Integer> table = new HashMap<>();
int calls = 0;
SlowPrices(Map<String, Integer> table) { this.table.putAll(table); }
public int price(String sku) {
calls++;
return table.get(sku);
}
void change(String sku, int amount) { table.put(sku, amount); } // the shop edits a price
}
static final class CachingPrices implements PriceService { // a caching proxy: asks the real service once per sku
private final PriceService real;
private final Map<String, Integer> seen = new HashMap<>();
CachingPrices(PriceService real) { this.real = real; }
public int price(String sku) { return seen.computeIfAbsent(sku, real::price); }
void invalidate(String sku) { seen.remove(sku); } // a cache that is never told to forget will be wrong
}
public static void main(String[] args) {
List<String> log = new ArrayList<>();
List<Photo> photos = new ArrayList<>();
for (int i = 1; i <= 5; i++) photos.add(new LazyPhoto("p" + i + ".jpg", log));
System.out.println("page opened with " + photos.size() + " photos, loads so far: " + log.size());
System.out.println("photo 1: " + photos.get(0).show());
System.out.println("photo 1 again: " + photos.get(0).show());
System.out.println("loads in total: " + log.size() + " (" + log.get(0) + ")");
SlowPrices slow = new SlowPrices(Map.of("p-1", 300));
CachingPrices prices = new CachingPrices(slow);
List<String> asked = new ArrayList<>();
for (int i = 0; i < 4; i++) asked.add(String.valueOf(prices.price("p-1")));
System.out.println("p-1 asked 4 times: " + String.join(" ", asked) + ", service calls: " + slow.calls);
slow.change("p-1", 320);
System.out.println("shop changes p-1 to 320, the cache says " + prices.price("p-1"));
prices.invalidate("p-1");
System.out.println("after invalidate: " + prices.price("p-1") + ", service calls: " + slow.calls);
}
}// proxies.cpp: a lazy photo and a caching price service, each with the same interface as the real thing
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
class Photo { // the subject's interface: all the page ever uses
public:
virtual ~Photo() = default;
virtual std::string show() = 0;
};
class RealPhoto : public Photo { // the real subject: loading happens in the constructor, which is the slow part
std::string file_;
public:
RealPhoto(std::string file, std::vector<std::string>& log) : file_(std::move(file)) { log.push_back("load " + file_); }
std::string show() override { return "showing " + file_; }
};
class LazyPhoto : public Photo { // a virtual proxy: builds the real photo on the first show()
std::string file_;
std::vector<std::string>& log_;
std::unique_ptr<Photo> real_;
public:
LazyPhoto(std::string file, std::vector<std::string>& log) : file_(std::move(file)), log_(log) {}
std::string show() override {
if (!real_) real_ = std::make_unique<RealPhoto>(file_, log_);
return real_->show();
}
};
class PriceService {
public:
virtual ~PriceService() = default;
virtual int price(const std::string& sku) = 0;
};
class SlowPrices : public PriceService { // the real subject: every call is a trip over the network
std::map<std::string, int> table_;
public:
int calls = 0;
explicit SlowPrices(std::map<std::string, int> table) : table_(std::move(table)) {}
int price(const std::string& sku) override {
++calls;
return table_.at(sku);
}
void change(const std::string& sku, int amount) { table_[sku] = amount; } // the shop edits a price
};
class CachingPrices : public PriceService { // a caching proxy: asks the real service once per sku
PriceService& real_;
std::map<std::string, int> seen_;
public:
explicit CachingPrices(PriceService& real) : real_(real) {}
int price(const std::string& sku) override {
auto it = seen_.find(sku);
if (it == seen_.end()) it = seen_.emplace(sku, real_.price(sku)).first;
return it->second;
}
void invalidate(const std::string& sku) { seen_.erase(sku); } // a cache that is never told to forget will be wrong
};
int main() {
std::vector<std::string> log;
std::vector<std::unique_ptr<Photo>> photos;
for (int i = 1; i <= 5; ++i) photos.push_back(std::make_unique<LazyPhoto>("p" + std::to_string(i) + ".jpg", log));
std::cout << "page opened with " << photos.size() << " photos, loads so far: " << log.size() << "\n";
std::cout << "photo 1: " << photos[0]->show() << "\n";
std::cout << "photo 1 again: " << photos[0]->show() << "\n";
std::cout << "loads in total: " << log.size() << " (" << log[0] << ")\n";
SlowPrices slow({{"p-1", 300}});
CachingPrices prices(slow);
std::string asked;
for (int i = 0; i < 4; ++i) asked += (i ? " " : "") + std::to_string(prices.price("p-1"));
std::cout << "p-1 asked 4 times: " << asked << ", service calls: " << slow.calls << "\n";
slow.change("p-1", 320);
std::cout << "shop changes p-1 to 320, the cache says " << prices.price("p-1") << "\n";
prices.invalidate("p-1");
int after = prices.price("p-1");
std::cout << "after invalidate: " << after << ", service calls: " << slow.calls << "\n";
}// proxies.js: a lazy photo and a caching price service, each with the same interface as the real thing
class RealPhoto { // the real subject: loading happens in the constructor, which is the slow part
constructor(file, log) {
log.push(`load ${file}`);
this.file = file;
}
show() { return `showing ${this.file}`; }
}
class LazyPhoto { // a virtual proxy: builds the real photo on the first show()
constructor(file, log) {
this.file = file;
this.log = log;
this.real = null;
}
show() {
if (this.real === null) this.real = new RealPhoto(this.file, this.log);
return this.real.show();
}
}
class SlowPrices { // the real subject: every call is a trip over the network
constructor(table) {
this.table = { ...table };
this.calls = 0;
}
price(sku) {
this.calls++;
return this.table[sku];
}
change(sku, amount) { this.table[sku] = amount; } // the shop edits a price
}
class CachingPrices { // a caching proxy: asks the real service once per sku
constructor(real) {
this.real = real;
this.seen = new Map();
}
price(sku) {
if (!this.seen.has(sku)) this.seen.set(sku, this.real.price(sku));
return this.seen.get(sku);
}
invalidate(sku) { this.seen.delete(sku); } // a cache that is never told to forget will be wrong
}
const log = [];
const photos = [1, 2, 3, 4, 5].map((i) => new LazyPhoto(`p${i}.jpg`, log));
console.log(`page opened with ${photos.length} photos, loads so far: ${log.length}`);
console.log(`photo 1: ${photos[0].show()}`);
console.log(`photo 1 again: ${photos[0].show()}`);
console.log(`loads in total: ${log.length} (${log[0]})`);
const slow = new SlowPrices({ "p-1": 300 });
const prices = new CachingPrices(slow);
const asked = [0, 1, 2, 3].map(() => prices.price("p-1"));
console.log(`p-1 asked 4 times: ${asked.join(" ")}, service calls: ${slow.calls}`);
slow.change("p-1", 320);
console.log(`shop changes p-1 to 320, the cache says ${prices.price("p-1")}`);
prices.invalidate("p-1");
console.log(`after invalidate: ${prices.price("p-1")}, service calls: ${slow.calls}`);page opened with 5 photos, loads so far: 0
photo 1: showing p1.jpg
photo 1 again: showing p1.jpg
loads in total: 1 (load p1.jpg)
p-1 asked 4 times: 300 300 300 300, service calls: 1
shop changes p-1 to 320, the cache says 300
after invalidate: 320, service calls: 2Check it by hand. Five lazy photos are built when the page opens, and 0 loads have happened. Photo 1 is shown: the proxy builds the real photo (the log says load p1.jpg) and shows it. Shown again, it reuses the real photo, so there is 1 load in total, not 5. For the price, four calls to prices.price("p-1") return 300 each time and the real service was called once. Then the shop changes the price to 320 and the cache still says 300, because nothing told it to forget. After invalidate, the next call asks the service again and gets 320, which is the service's second call.
The lazy photo creates the real photo on the first show, which loads the file, then forwards. On the second show it only forwards: two looks, one load.
The last three lines are the cost of a caching proxy, and they are the part interviewers probe. A cache is a promise that the answer hasn't changed, and the proxy has no way of knowing when it has. So a real caching proxy needs a rule: an expiry time, an invalidation call that the writer makes (as here), or both. Each language brings something to the picture:
- Python has no
interfacekeyword, so the abstract base classes document the subject, andLazyPhotowould work with plain duck typing as well. A cache for a function (not a class) is one line,functools.lru_cache, which does a caching proxy's job with Python's decorator syntax. - Java makes
PhotoandPriceServiceinterfaces, andMap.computeIfAbsentis a one-line cache fill. TheRealPhotoconstructor runs only when the proxy decides. - C++ holds the real photo in a
std::unique_ptr, which starts empty, so "not loaded yet" is the null pointer and the destructor of the proxy releases it for free. - JavaScript keeps the real photo in a field that starts as
null. For anasyncload, store the promise and not the result: two callers that arrive before the load finishes then share one load.
A caching proxy remembers 300 and cannot know the shop changed the price to 320. It keeps saying 300, so a cache needs an expiry or invalidation.
The kinds of proxy#
The book's applicability section lists four reasons to put a stand-in in front of an object. A fifth, the caching proxy, is the one you meet most. The book has no separate name for it, and it is best seen as a smart reference whose extra action is remembering:
| Kind | What it controls | Here | Real example |
|---|---|---|---|
| Virtual | When an expensive object is created | LazyPhoto |
An ORM's lazy-loaded relationship, which fetches the rows on first access |
| Protection | Who may call what | Guard, below |
Collections.unmodifiableList in Java, MappingProxyType in Python: read-only views that refuse writes |
| Remote | Where the object lives | Not coded | A gRPC client stub or Java RMI stub: a local method that serialises the call and sends it |
| Smart reference | What else happens on access: counting, locking, loading | The viewer counter, below | std::shared_ptr, which counts owners and frees on the last one |
| Caching | How often the real work happens | CachingPrices |
An HTTP caching reverse proxy or a CDN edge in front of an origin |
Two of these cross a boundary and deserve a sentence. A remote proxy is why calling a service can look like calling a local object: the stub has the same interface and hides the network, which is also why it can fail in ways a local object can't (a timeout, a retry, a partial failure, as in the Week 2 lessons). And an HTTP caching proxy is the same pattern at the scale of a network: the browser asks for a page, and the proxy answers from its copy if it has one. The course covers that in the caching week.
A protection proxy, built by the language#
A protection proxy checks a call before forwarding it. You can write one by hand, one forwarding method per method of the interface, and the C++ version below does. The other three languages can build one that doesn't name the methods it forwards, which is how a single guard can protect any object. This program guards a price book, so a clerk may read prices and only an admin may change them, and counts every call that goes through:
# guard.py: a protection proxy built with __getattr__, so it forwards methods it never names
class PriceBook: # the real subject
def __init__(self, prices) -> None:
self.prices = dict(prices)
def price(self, sku: str) -> int:
return self.prices[sku]
def set_price(self, sku: str, amount: int) -> None:
self.prices[sku] = amount
class Guard: # a protection proxy for any object: it counts every call and checks the role
def __init__(self, target, role: str, admin_only) -> None:
self._target, self._role, self._admin_only, self.calls = target, role, admin_only, 0
def __getattr__(self, name: str): # runs only for names that Guard itself does not have
member = getattr(self._target, name)
if not callable(member): # data stays behind the guard: clerk.prices would hand out the real dict
raise AttributeError(f"{name} is not exposed")
def call(*args):
self.calls += 1
if name in self._admin_only and self._role != "admin":
raise PermissionError("needs admin")
return member(*args)
return call
book = PriceBook({"p-1": 300})
clerk, admin = Guard(book, "clerk", {"set_price"}), Guard(book, "admin", {"set_price"})
print(f"clerk reads p-1: {clerk.price('p-1')}")
try:
clerk.set_price("p-1", 250)
except PermissionError as e:
print(f"clerk sets p-1: refused, {e}")
admin.set_price("p-1", 250)
print("admin sets p-1 to 250: ok")
print(f"admin reads p-1: {admin.price('p-1')}")
print(f"calls through the clerk proxy: {clerk.calls}, through the admin proxy: {admin.calls}")// Guard.java: a protection proxy built with java.lang.reflect.Proxy, so it forwards methods it never names
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
class Guard {
interface PriceBook { // a dynamic proxy needs an interface to implement
int price(String sku);
void setPrice(String sku, int amount);
}
static final class RealBook implements PriceBook { // the real subject
private final Map<String, Integer> prices = new HashMap<>();
RealBook(Map<String, Integer> prices) { this.prices.putAll(prices); }
public int price(String sku) { return prices.get(sku); }
public void setPrice(String sku, int amount) { prices.put(sku, amount); }
}
static final class Check implements InvocationHandler { // runs for every call on the proxy, whatever the method is
private final Object target;
private final String role;
private final Set<String> adminOnly;
int calls = 0;
Check(Object target, String role, Set<String> adminOnly) {
this.target = target;
this.role = role;
this.adminOnly = adminOnly;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
calls++;
if (adminOnly.contains(method.getName()) && !role.equals("admin")) throw new SecurityException("needs admin");
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
}
static PriceBook guard(PriceBook target, Check check) {
return (PriceBook) Proxy.newProxyInstance(PriceBook.class.getClassLoader(), new Class<?>[] {PriceBook.class}, check);
}
public static void main(String[] args) {
PriceBook book = new RealBook(Map.of("p-1", 300));
Check clerkCheck = new Check(book, "clerk", Set.of("setPrice"));
Check adminCheck = new Check(book, "admin", Set.of("setPrice"));
PriceBook clerk = guard(book, clerkCheck);
PriceBook admin = guard(book, adminCheck);
System.out.println("clerk reads p-1: " + clerk.price("p-1"));
try {
clerk.setPrice("p-1", 250);
} catch (SecurityException e) {
System.out.println("clerk sets p-1: refused, " + e.getMessage());
}
admin.setPrice("p-1", 250);
System.out.println("admin sets p-1 to 250: ok");
System.out.println("admin reads p-1: " + admin.price("p-1"));
System.out.println("calls through the clerk proxy: " + clerkCheck.calls + ", through the admin proxy: " + adminCheck.calls);
}
}// guard.cpp: a protection proxy in C++ is written by hand: there is no reflection to forward calls we never named
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
class PriceBook { // the subject's interface
public:
virtual ~PriceBook() = default;
virtual int price(const std::string& sku) = 0;
virtual void set_price(const std::string& sku, int amount) = 0;
};
class RealBook : public PriceBook { // the real subject
std::map<std::string, int> prices_;
public:
explicit RealBook(std::map<std::string, int> prices) : prices_(std::move(prices)) {}
int price(const std::string& sku) override { return prices_.at(sku); }
void set_price(const std::string& sku, int amount) override { prices_[sku] = amount; }
};
class Guard : public PriceBook { // a protection proxy: one forwarding method per method of the interface
PriceBook& target_;
std::string role_;
public:
int calls = 0;
Guard(PriceBook& target, std::string role) : target_(target), role_(std::move(role)) {}
int price(const std::string& sku) override {
++calls;
return target_.price(sku);
}
void set_price(const std::string& sku, int amount) override {
++calls;
if (role_ != "admin") throw std::runtime_error("needs admin");
target_.set_price(sku, amount);
}
};
int main() {
RealBook book({{"p-1", 300}});
Guard clerk(book, "clerk"), admin(book, "admin");
std::cout << "clerk reads p-1: " << clerk.price("p-1") << "\n";
try {
clerk.set_price("p-1", 250);
} catch (const std::runtime_error& e) {
std::cout << "clerk sets p-1: refused, " << e.what() << "\n";
}
admin.set_price("p-1", 250);
std::cout << "admin sets p-1 to 250: ok\n";
std::cout << "admin reads p-1: " << admin.price("p-1") << "\n";
std::cout << "calls through the clerk proxy: " << clerk.calls << ", through the admin proxy: " << admin.calls << "\n";
}// guard.js: a protection proxy built with the built-in Proxy, so it forwards methods it never names
class PriceBook { // the real subject
constructor(prices) {
this.prices = { ...prices };
}
price(sku) { return this.prices[sku]; }
setPrice(sku, amount) { this.prices[sku] = amount; }
}
function guard(target, role, adminOnly) {
const stats = { calls: 0 };
const proxy = new Proxy(target, {
get(real, name) { // the trap runs for every property read, and a method call starts as a read
const member = real[name];
if (typeof member !== "function") return undefined; // data stays behind the guard
return (...args) => {
stats.calls++;
if (adminOnly.has(name) && role !== "admin") throw new Error("needs admin");
return member.apply(real, args);
};
},
set() { throw new Error("read-only"); }, // without this trap, clerk.prices = {} would write to the real book
});
return { proxy, stats };
}
const book = new PriceBook({ "p-1": 300 });
const { proxy: clerk, stats: clerkStats } = guard(book, "clerk", new Set(["setPrice"]));
const { proxy: admin, stats: adminStats } = guard(book, "admin", new Set(["setPrice"]));
console.log(`clerk reads p-1: ${clerk.price("p-1")}`);
try {
clerk.setPrice("p-1", 250);
} catch (e) {
console.log(`clerk sets p-1: refused, ${e.message}`);
}
admin.setPrice("p-1", 250);
console.log("admin sets p-1 to 250: ok");
console.log(`admin reads p-1: ${admin.price("p-1")}`);
console.log(`calls through the clerk proxy: ${clerkStats.calls}, through the admin proxy: ${adminStats.calls}`);clerk reads p-1: 300
clerk sets p-1: refused, needs admin
admin sets p-1 to 250: ok
admin reads p-1: 250
calls through the clerk proxy: 2, through the admin proxy: 2The clerk's two calls (a read and a refused write) and the admin's two (a write and a read) are all counted, including the refused one, because the proxy sees it before it decides. The real price book never saw the refused call. What each language does:
- Python uses
__getattr__, which runs only when normal attribute lookup fails, soGuardforwards every name it doesn't have itself. It has gaps. Python looks up special methods (len(proxy), iteration,with, operators) on the type, not through__getattr__, so a proxy that must support them has to define them: calllen()on this one and you get aTypeError. It forwards data as readily as methods, which is why the guard refuses non-callables (a plain forwarder would hand a clerk the realpricesdict to edit). Andisinstance(clerk, PriceBook)is false. Nothing is private in Python, soclerk._targetstill reaches the book: a guard like this stops mistakes, not an attacker. - Java has
java.lang.reflect.Proxy, which builds a class at run time that implements interfaces and sends every call to oneInvocationHandler.invoke(proxy, method, args). It can't proxy a class: passArrayList.classand it throwsIllegalArgumentException("not an interface").equals,hashCodeandtoStringgo throughinvoketoo, so a counting handler counts them. Frameworks use proxies like this to put a transaction or a security check around a method, and fall back to a generated subclass when there is no interface. - JavaScript has
Proxywith traps:get(which sees a method call, because a call starts as a property read),set,has,deleteProperty,apply(for a function target) and more. A trap you don't write forwards to the target, which is why the guard writesset: without it a write through the proxy lands on the real book. Two more to know: the proxy is a different object from its target (proxy === targetis false), and a method that touches a#privatefield, or a built-in such asMapwith internal slots, throws aTypeErrorif it runs withthisset to the proxy, which is why the guard callsmember.apply(real, args). - C++ has no reflection in C++17, so the guard is written by hand: one method forwards each call, here two. That is the cost the other languages avoid. A class that overloads
operator->can wrap every call without naming the methods, but it can't see the method's name, which is how smart pointers work and is enough for counting, not for "onlyset_priceneeds admin".
Which kind is it?
A class wraps a Report object, and calls to delete() are refused unless the caller is an owner. The wrapper has the same methods as Report. Is it a proxy or a decorator, and which kind?
One strong answer
A proxy: a protection proxy. It has the same interface and controls access to the real object. If it added a new responsibility instead (an audit log around every call, for example), it would be a decorator. The book notes that a protection proxy can be implemented exactly like a decorator: the structure doesn't tell them apart, the purpose does.
A smart reference: count the viewers, free the last#
A smart reference does something extra when the object is reached. This one counts viewers, so the photo is loaded when the first viewer opens it and released when the last one closes:
# smart_reference.py: a handle that counts viewers, so the photo is loaded on the first open and released on the last close
class Handle: # what a viewer holds, in place of the photo itself
def __init__(self, cache, file: str) -> None:
self.cache, self.file, self.closed = cache, file, False
def close(self) -> None:
if not self.closed:
self.closed = True
self.cache.release(self.file)
class PhotoCache:
def __init__(self, log: list) -> None:
self.log, self.counts = log, {}
def open(self, file: str) -> Handle:
if file not in self.counts:
self.log.append(f"load {file}")
self.counts[file] = 0
self.counts[file] += 1
return Handle(self, file)
def release(self, file: str) -> None:
self.counts[file] -= 1
if self.counts[file] == 0:
del self.counts[file]
self.log.append(f"release {file}")
def viewers(self, file: str) -> int:
return self.counts.get(file, 0)
log = []
cache = PhotoCache(log)
first = cache.open("p1.jpg")
print(f"open p1.jpg: viewers {cache.viewers('p1.jpg')}")
second = cache.open("p1.jpg")
print(f"open p1.jpg: viewers {cache.viewers('p1.jpg')}")
first.close()
print(f"close: viewers {cache.viewers('p1.jpg')}")
second.close()
print(f"close: viewers {cache.viewers('p1.jpg')}")
print(f"log: {', '.join(log)}")// SmartReference.java: a handle that counts viewers, so the photo is loaded on the first open and released on the last close
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class SmartReference {
static final class Handle { // what a viewer holds, in place of the photo itself
private final PhotoCache cache;
private final String file;
private boolean closed = false;
Handle(PhotoCache cache, String file) {
this.cache = cache;
this.file = file;
}
void close() {
if (!closed) {
closed = true;
cache.release(file);
}
}
}
static final class PhotoCache {
private final List<String> log;
private final Map<String, Integer> counts = new HashMap<>();
PhotoCache(List<String> log) { this.log = log; }
Handle open(String file) {
if (!counts.containsKey(file)) {
log.add("load " + file);
counts.put(file, 0);
}
counts.merge(file, 1, Integer::sum);
return new Handle(this, file);
}
void release(String file) {
int left = counts.merge(file, -1, Integer::sum);
if (left == 0) {
counts.remove(file);
log.add("release " + file);
}
}
int viewers(String file) { return counts.getOrDefault(file, 0); }
}
public static void main(String[] args) {
List<String> log = new ArrayList<>();
PhotoCache cache = new PhotoCache(log);
Handle first = cache.open("p1.jpg");
System.out.println("open p1.jpg: viewers " + cache.viewers("p1.jpg"));
Handle second = cache.open("p1.jpg");
System.out.println("open p1.jpg: viewers " + cache.viewers("p1.jpg"));
first.close();
System.out.println("close: viewers " + cache.viewers("p1.jpg"));
second.close();
System.out.println("close: viewers " + cache.viewers("p1.jpg"));
System.out.println("log: " + String.join(", ", log));
}
}// smart_reference.cpp: std::shared_ptr is the smart reference: it counts viewers, and its deleter runs on the last close
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <vector>
struct Photo {
std::string file;
};
class PhotoCache {
std::vector<std::string>& log_;
std::map<std::string, std::weak_ptr<Photo>> live_; // weak: the cache does not keep a photo alive by itself
public:
explicit PhotoCache(std::vector<std::string>& log) : log_(log) {}
std::shared_ptr<Photo> open(const std::string& file) {
if (auto photo = live_[file].lock()) return photo; // already loaded: share it
log_.push_back("load " + file);
std::vector<std::string>& log = log_;
std::shared_ptr<Photo> photo(new Photo{file}, [&log](Photo* p) { // runs when the last viewer lets go
log.push_back("release " + p->file);
delete p;
});
live_[file] = photo;
return photo;
}
long viewers(const std::string& file) const {
auto it = live_.find(file);
return it == live_.end() ? 0 : it->second.use_count();
}
};
int main() {
std::vector<std::string> log;
PhotoCache cache(log);
auto first = cache.open("p1.jpg");
std::cout << "open p1.jpg: viewers " << cache.viewers("p1.jpg") << "\n";
auto second = cache.open("p1.jpg");
std::cout << "open p1.jpg: viewers " << cache.viewers("p1.jpg") << "\n";
first.reset();
std::cout << "close: viewers " << cache.viewers("p1.jpg") << "\n";
second.reset();
std::cout << "close: viewers " << cache.viewers("p1.jpg") << "\n";
std::string joined;
for (const auto& entry : log) joined += (joined.empty() ? "" : ", ") + entry;
std::cout << "log: " << joined << "\n";
}// smartReference.js: a handle that counts viewers, so the photo is loaded on the first open and released on the last close
class Handle { // what a viewer holds, in place of the photo itself
constructor(cache, file) {
this.cache = cache;
this.file = file;
this.closed = false;
}
close() {
if (!this.closed) {
this.closed = true;
this.cache.release(this.file);
}
}
}
class PhotoCache {
constructor(log) {
this.log = log;
this.counts = new Map();
}
open(file) {
if (!this.counts.has(file)) {
this.log.push(`load ${file}`);
this.counts.set(file, 0);
}
this.counts.set(file, this.counts.get(file) + 1);
return new Handle(this, file);
}
release(file) {
const left = this.counts.get(file) - 1;
this.counts.set(file, left);
if (left === 0) {
this.counts.delete(file);
this.log.push(`release ${file}`);
}
}
viewers(file) { return this.counts.get(file) ?? 0; }
}
const log = [];
const cache = new PhotoCache(log);
const first = cache.open("p1.jpg");
console.log(`open p1.jpg: viewers ${cache.viewers("p1.jpg")}`);
const second = cache.open("p1.jpg");
console.log(`open p1.jpg: viewers ${cache.viewers("p1.jpg")}`);
first.close();
console.log(`close: viewers ${cache.viewers("p1.jpg")}`);
second.close();
console.log(`close: viewers ${cache.viewers("p1.jpg")}`);
console.log(`log: ${log.join(", ")}`);open p1.jpg: viewers 1
open p1.jpg: viewers 2
close: viewers 1
close: viewers 0
log: load p1.jpg, release p1.jpgTwo viewers open the same photo: one load, and a count of 2. The first closes (count 1, nothing released), and the second closes (count 0, and only now is the photo released). In Python, Java and JavaScript the count is kept by hand, in a handle with a close(). In C++ it is free: std::shared_ptr is the smart reference, its use_count() is the viewer count, its deleter is the release, and the cache holds weak_ptrs so that the cache itself doesn't keep a photo alive. Be precise about what it stands in for: a shared_ptr has a pointer's interface (-> and *), not the photo's methods, so it is the book's smart pointer, a proxy for a raw pointer, and not a proxy with the Photo interface. Python's weakref.proxy is a proxy that doesn't keep its target alive. In Java the garbage collector decides when memory is freed, so "release on the last close" needs an explicit close(), ideally behind try-with-resources.
Proxy, Decorator and Adapter#
All three put an object in front of another one, and interviewers ask which is which. The test is intent, because the structures can look the same.
| Proxy | Decorator (lesson 11) | Adapter (lesson 10) | |
|---|---|---|---|
| Interface | Same as the real object | Same as what it wraps | Different: it translates one into another |
| Purpose | Control access to the real object | Add a responsibility to it | Make it fit an interface the client expects |
| Who builds it | Often the proxy itself creates and owns the real subject (LazyPhoto builds RealPhoto) |
The client builds the stack and chooses what to wrap | The client, or the wiring, around a class it can't change |
| How many | Usually one in front of one subject | Many, stacked in any order | One per adaptee |
| Does the client know? | No: it uses the subject interface | Yes: it chose the layers | Yes: it chose the adapter |
| Here | LazyPhoto, CachingPrices, Guard |
Retry, Logging, Surge |
AlphaAdapter |
The book itself says the line is blurry in one place: a protection proxy can be implemented exactly like a decorator. Then ask what the wrapper is for. Refusing a call, deferring a load or remembering an answer is control of access. Adding a log line, a retry or a surcharge is a new responsibility. And a remote proxy doesn't contain a direct reference to its real subject at all: it holds an address.
A proxy and a decorator keep the interface, so tell them apart by purpose: a proxy controls access, a decorator adds a duty. An adapter changes the interface.
When not to use it#
- When the stand-in changes what the object means. A lazy proxy that fails on first use, or a cache that returns stale data, breaks the promise that it behaves like the real thing. A proxy must keep the subject's contract, or it is a different class.
- When a cache has no invalidation. The run above shows 300 after the price became 320. Decide on an expiry, an invalidation call or both before you write the cache.
- When laziness moves a delay to a bad moment. A lazy photo loads on the first
show(), which is the moment the shopper is looking at the screen. Sometimes the right answer is to pre-load the next one. - When two threads can reach a lazy proxy, unguarded.
LazyPhotochecksreal is Noneand then builds, so two threads can both seeNoneand load twice. In Java and C++ it can be worse: without a lock or avolatile/atomic field, a thread can see the reference before the object behind it is fully built. Make check-and-create one step: a lock (and check again inside it),std::call_oncein C++, asynchronizedmethod or avolatilefield with double-checked locking in Java, and inasyncJavaScript store the promise so every caller awaits one load. - When identity matters. A proxy is a different object from the real one.
proxy == realandproxy is realare false, a type check fails, and a cache keyed on object identity sees two entries. - When a plain function does it. One call to cache is
functools.lru_cache, and a one-off permission check is anif. A class pair is for when the same interface needs controlling in several places. - When a dynamic proxy hides the bug. Reflection-based proxies make stack traces longer and calls harder to step through. Use them when the number of methods makes hand-written forwarding impractical.
Where it shows up in the case studies#
One round has the pattern in its code, and one has the word without the pattern.
| Case study | What it has | What to say |
|---|---|---|
| Key-value store | snapshot() returns a MappingProxyType |
A protection proxy: a read-only view with the same read interface as a dict, which refuses every write (assigning to one raises TypeError). A MappingProxyType is a live view of whatever it wraps, and here that is a fresh copy built under the store's lock, so the caller holds a stable, read-only picture and can't change the store's data through it |
| Online auction | "Proxy bidding" in the follow-ups | Not this pattern. It is a domain term: a bidder sets a secret maximum and the system bids for them up to it. Nothing there is a stand-in with the same interface as another object |
In the interview#
Say it with the catch: "Proxy puts a stand-in with the same interface in front of an object to control access to it: create it lazily, check permissions, cache answers or cross a network. The client can't tell it from the real thing. The catch is that it must keep the real object's contract, and a cache needs a rule for when it is wrong."
Then expect the follow-ups:
- "Proxy or Decorator?" The structure is the same, and the purpose differs. A proxy controls access to its subject, and often creates and owns it. A decorator adds a responsibility, and the client stacks several.
- "Proxy or Adapter?" An adapter changes the interface to fit what the client expects. A proxy keeps the interface the same.
- "Name the kinds." Virtual, protection, remote and smart reference, and the caching proxy, which most real ones are.
- "How would you build a generic one?" Python
__getattr__, Java'sjava.lang.reflect.Proxyfor interfaces, JavaScript'sProxywith traps, and in C++ by hand or withoperator->for call-name-free wrapping. - "What goes wrong with a caching proxy?" Stale data, so you need an expiry or an invalidation call. Also unbounded growth, and two callers racing to fill the same entry.
- "Is
MappingProxyTypea proxy?" Yes, a protection proxy: a read-only view of a mapping, with writes refused.
Common mistakes#
- Calling every wrapper a proxy. A wrapper that adds retries or a surcharge is a decorator. A wrapper that translates is an adapter.
- A proxy with a different interface. Then the client has to know it is there, and the pattern is gone.
- A cache with no invalidation or expiry, so the shop changes a price and the app shows the old one.
- Lazy initialisation that isn't thread-safe, which loads twice or hands out a half-built object.
- Expecting identity to survive.
proxy == realis false, and so isisinstance(proxy, RealPhoto). - Calling "proxy bidding" the Proxy pattern. It is an automatic bid up to a maximum, and not a stand-in.
Proxy
Q1/4 A class has the same methods as
Reportand refusesdelete()unless the caller is an owner. What is it?Answer & explanation
Answer: C
A proxy keeps the interface and controls access. An adapter changes the interface, a decorator adds a responsibility, and a facade fronts several classes with a new, simpler interface.
Q2/4 The first
show()on a lazy photo builds the real one, and the second reuses it. After showing photo 1 twice on a page of five, how many loads happened?Answer & explanation
Answer: C
Four photos were never looked at, so they were never built. Photo 1 was built on its first
show()and reused on its second, which is the point of a virtual proxy.Q3/4 The shop changes a price from 300 to 320, and the app still shows 300. What is the cause, and what would fix it?
Answer & explanation
Answer: C
A cache is a promise that an answer hasn't changed. The proxy holds the old answer until it is told to forget it or its time is up. The real service is right and the interface is right: the rule for forgetting is missing.
Q4/4 Which statement about Proxy and Decorator is right?
Answer & explanation
Answer: B
Both implement the interface they wrap and hold a reference to a real object, so the code can look identical. Many proxies do create and own their subject, but not all do, and a remote proxy holds an address. Neither changes the interface: that is the adapter.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


