Skip to main content
CleanCodeMastery

Proxy Pattern: The Watchman Who Checks Before You Meet the Owner

Learn the Proxy pattern with a society watchman story. Put a stand-in with the same interface in front of a real object to control access to it.

24 min read Updated June 11, 2026beginner
design-patternsstructural-patternsproxylazy-loadingaccess-controltypescriptcsharp

🚪 The watchman at the owner's bungalow

In your town there is a big bungalow belonging to Mr. Mehta, a busy factory owner. You cannot just walk in and meet him. At the gate sits Ram Singh, the watchman, with his thick visitor register and his folding chair.

Today, fourteen-year-old Riya arrives. Her school is collecting donations for the science fair, and someone said Mehta sir gives generously. Watch what Ram Singh does. She says, "I want to meet Mehta sir." He asks her name and her purpose. He checks his visitor list. School donation collectors are on the approved list, so he notes her entry time in the register and only then sends word inside.

Ten minutes later a salesman arrives with a suitcase of water purifiers. Same gate, same words: "I want to meet Mehta sir." Ram Singh checks the list. Salesmen are not on it. The man is politely turned away — and here is the key part — Mr. Mehta is never even disturbed. He does not know the salesman existed.

There is more. Sometimes Mr. Mehta is not even in the bungalow; he comes only when there is real work. The visitors standing outside cannot tell the difference — they talk to the gate either way. The gate answers small repeated questions itself ("Sir is away till Monday") without calling the owner at all. Ram Singh remembers the answer from the morning and repeats it; no need to ring the bell every time.

Notice the most important detail: for the visitor, the gate IS the bungalow. You make your request at the gate exactly as you would to the owner. Same words, same requests. But behind that same face, the watchman decides three things: whether your request goes in, when it goes in, and what gets written down about it.

The watchman never does the owner's work. He never signs business deals. He only controls access to the man who does.

In software, this watchman is called a Proxy. It stands in front of a real object, wears the same interface, and intercepts every call — checking, delaying, recording, or refusing — before the real object is touched.

Figure 1: Two visitors at the gate, as a journey

Read the last line of the journey. The owner's day is perfect because the gate absorbed both visits — one allowed, one refused — without him lifting a finger. That undisturbed owner is your heavy, precious real object.

What is the Proxy pattern?

Proxy is a structural design pattern that puts a stand-in (surrogate) object in front of a real object. The proxy implements the same interface as the real object, so client code cannot tell them apart. Because every call passes through the proxy first, the proxy can do extra work before, after, or instead of forwarding the call:

  • delay creating the heavy real object until it is truly needed (virtual proxy / lazy loading),
  • check who is calling and refuse forbidden requests (protection proxy / access control),
  • remember answers to repeated questions (caching proxy),
  • write a register of every call (logging proxy),
  • or carry the call across a network to an object on another machine (remote proxy).

The defining sentence is short: same interface, controlled access. A proxy never changes what the object does. It changes when, whether, and under what conditions the real work happens.

💡

Quick test to identify a proxy: does the wrapper offer the exact same methods as the real thing, and does it mainly decide whether/when the call reaches the real thing? Then it is a proxy. If it offers new, simpler methods over many objects, it is a Facade. If it adds behaviour the client stacks on purpose, it is a Decorator.

The pattern's old name is Surrogate — a substitute that acts on behalf of the real subject.

The four classic watchman duties, side by side:

Proxy typeWhat it controlsRam Singh versionSoftware example
VirtualWhen the real object is createdCalls the owner only when there is real workPhoto loads on first view; ORM lazy fields
ProtectionWho may get throughChecks the visitor list, refuses salesmenAdmin-only delete; role checks
RemoteWhere the real object livesPasses messages to an owner who is out of towngRPC stubs, RMI, API clients
SmartWhat gets recorded or reusedWrites the register, repeats known answersCaching, logging, reference counting

College corner — the four classic types, formally: a virtual proxy manages creation time: it holds enough information to build the real subject (a path, an ID) and defers the expensive constructor until first use — this is lazy initialization wrapped in an object. A protection proxy manages authority: it evaluates the caller's rights before forwarding, keeping authorization concerns out of the domain class entirely. A remote proxy manages location: the real subject lives in another process or on another machine, and the proxy marshals arguments over the wire while preserving the local-call illusion (this is how gRPC stubs, RMI, and old CORBA all work — and why a "method call" can suddenly throw a network timeout). A smart proxy manages bookkeeping: caching, logging, metrics, reference counting. Real frameworks usually generate these at runtime: Java's java.lang.reflect.Proxy, .NET's Castle DynamicProxy (used by Moq and EF), and JavaScript's built-in Proxy object all create watchmen for you from an interface description.

The problem it solves

Suppose Riya's school app shows a gallery of 1,000 annual-day photos. Each photo is high resolution; constructing one object means reading and decoding megabytes from disk:

// The heavy real object — expensive the moment it is constructed
class HighResPhoto {
  private pixels: Uint8Array;
 
  constructor(private path: string) {
    console.log(`Loading ${path}... (slow! megabytes from disk)`);
    this.pixels = new Uint8Array(5_000_000); // pretend decode
  }
 
  display(): void {
    console.log(`Displaying ${this.path}`);
  }
}
 
// Gallery start-up — the naive way
const photos = paths.map((p) => new HighResPhoto(p)); // loads ALL 1000!

The user opens the gallery, and the app freezes for half a minute and eats gigabytes of RAM — to load a thousand photos when the user will scroll past and actually view maybe ten. You are paying the full cost of every object up front, for objects that may never be used. It is like Mr. Mehta personally standing at the gate all day in case someone visits.

You could litter the gallery code with if (!loaded) load(); checks, but then loading logic smears across every call site. And the same shape of problem appears again and again in other clothes:

  • Only the principal's account should be able to delete photos — but the photo class should not know about logins.
  • You want a log of who viewed what — without editing the photo class.
  • The photos might live on a server — but the gallery wants to call them like local objects.

In every case, you want to interpose behaviour around an object, without touching the object and without the client noticing.

Figure 2: Loading everything up front vs proxies that load on demand

Here is the startup-time difference, measured. The naive line grows with every photo in the gallery; the proxy line stays flat because creating a watchman costs nearly nothing:

Figure 3: Gallery startup time as the photo count grows

And after a typical session, look at how little work was ever actually needed:

Figure 4: Fate of 1000 photo proxies in one session

Almost 99% of the heavy objects were never created at all. That huge "never loaded" slice is the virtual proxy's entire salary. Lazy loading wins exactly when many are created but few are used — galleries, feeds, ORM relations, plugin systems.

⚙️ How it works, step by step

  1. Define a Subject interface that captures what clients call — display(), remove(). If the real class has no interface yet, extract one.
  2. Keep the RealSubject as is. HighResPhoto keeps doing the real, heavy work. It needs no edits — that is half the beauty.
  3. Create the Proxy class implementing the same interface. It stores whatever it needs to get the real subject later — here, just the file path — plus a reference that starts as null.
  4. Implement each method with "extra work + delegate". Check access. If allowed, lazily build the real subject on first use, then forward the call. Refuse, log, or answer from cache as required.
  5. Hand clients the proxy instead of the real object. A factory or the wiring code does this; the client just programs against the Subject interface and never knows.
Figure 5: Class structure of the Proxy pattern

The diagram's key line is the bottom one: the Gallery depends only on the Photo interface. Whether it holds a watchman or the owner, it cannot tell — and that is the point.

The lazy life of the real subject behind one proxy is a tiny state machine, and it is worth memorising, because it explains both the pattern's biggest win (cheap start) and its biggest surprise (slow first call):

Figure 6: Lifecycle of the real object behind a virtual proxy

Two details deserve a second look. The NotLoaded → NotLoaded self-loop is the protection proxy at work: a denied request bounces off the gate and the expensive Loading state is never entered. And the single Loading transition is where the hidden first-call delay lives — the cost did not vanish, it moved to the first real use.

College corner — lazy initialization, done properly: the proxy's if (real == null) real = new RealSubject() line is fine in single-threaded code, but in a multi-threaded server two threads can race through the null check and build the heavy object twice (or worse, one thread sees a half-built object). The classic fixes: lock the whole accessor (simple, slight cost), double-checked locking with a volatile field (fast, fiddly to get right), or language-level lazy holders — Lazy<T> in C#, lazy val in Scala/Kotlin, the initialization-on-demand holder idiom in Java. Rule of thumb for coursework and interviews: say "I would use the platform's lazy primitive instead of hand-rolling double-checked locking," and you will sound like someone who has been burned before.

Real-life code example

The school gallery in TypeScript, showing the two most useful proxy jobs together: lazy loading and access control (with a visitor register as a bonus).

// ----- The Subject interface: what every "photo" must offer -----
interface Photo {
  display(user: string): void;
  remove(user: string): void;
}
 
// ----- The RealSubject: heavy, honest, knows nothing of security -----
class HighResPhoto implements Photo {
  private pixels: Uint8Array;
 
  constructor(private path: string) {
    // Imagine megabytes read and decoded here.
    console.log(`  (slow) Loading ${this.path} from disk...`);
    this.pixels = new Uint8Array(5_000_000);
  }
 
  display(user: string): void {
    console.log(`  Showing ${this.path} (${this.pixels.length} bytes)`);
  }
 
  remove(user: string): void {
    console.log(`  ${this.path} deleted from disk.`);
  }
}
 
// ----- The Proxy: same interface, watchman duties -----
class PhotoProxy implements Photo {
  private real: HighResPhoto | null = null;  // LAZY: not loaded yet!
 
  constructor(
    private path: string,
    private admins: string[],                // who may delete
  ) {}
 
  // Virtual proxy part: build the real object only on first need.
  private getReal(): HighResPhoto {
    if (this.real === null) {
      this.real = new HighResPhoto(this.path);
    }
    return this.real;
  }
 
  display(user: string): void {
    console.log(`[Watchman] ${user} wants to view ${this.path}. Noted in register.`);
    this.getReal().display(user);            // load now (if not already), then delegate
  }
 
  // Protection proxy part: check before letting the call through.
  remove(user: string): void {
    if (!this.admins.includes(user)) {
      console.log(`[Watchman] DENIED. ${user} cannot delete ${this.path}.`);
      return;                                 // real object never disturbed
    }
    console.log(`[Watchman] ${user} is admin. Allowing delete.`);
    this.getReal().remove(user);
  }
}
 
// ----- The client: builds 1000 photos in a blink -----
const admins = ["principal"];
const gallery: Photo[] = [];
 
console.log("Opening gallery with 1000 photos...");
for (let i = 1; i <= 1000; i++) {
  gallery.push(new PhotoProxy(`annual-day-${i}.jpg`, admins));
}
console.log("Gallery open instantly! Nothing loaded yet.\n");
 
// Riya views just two photos — only those two load.
gallery[0].display("riya");
gallery[0].display("riya");     // second view: already loaded, no slow line
gallery[41].display("arjun");
 
console.log("");
 
// Access control in action.
gallery[0].remove("riya");          // student — denied
gallery[0].remove("principal");     // admin — allowed

Output:

Opening gallery with 1000 photos...
Gallery open instantly! Nothing loaded yet.
 
[Watchman] riya wants to view annual-day-1.jpg. Noted in register.
  (slow) Loading annual-day-1.jpg from disk...
  Showing annual-day-1.jpg (5000000 bytes)
[Watchman] riya wants to view annual-day-1.jpg. Noted in register.
  Showing annual-day-1.jpg (5000000 bytes)
[Watchman] arjun wants to view annual-day-42.jpg. Noted in register.
  (slow) Loading annual-day-42.jpg from disk...
  Showing annual-day-42.jpg (5000000 bytes)
 
[Watchman] riya cannot delete annual-day-1.jpg. DENIED.
[Watchman] principal is admin. Allowing delete.
  (slow) ... already loaded ...
  annual-day-1.jpg deleted from disk.

Trace the three wins:

  1. Lazy loading. We built 1,000 photo objects, yet the slow "Loading..." line printed only twice — for the two photos actually viewed. 998 heavy objects were never created.
  2. Reuse. Riya's second view shows no loading line. The proxy kept the already-built real object and just delegated.
  3. Access control. Riya's delete was refused at the gate — the real photo object was never even touched. The principal's delete passed through. And HighResPhoto contains zero security code; it stays clean and reusable.

Here is Riya's pair of views as a sequence — the first one pays the loading cost, the second one rides the cache:

Figure 7: First view loads, second view is served from cache

All of this, and the gallery code never learned a thing — it holds Photo references and calls the same two methods throughout.

The same idea in C#

A compact C# version of the virtual + protection proxy:

public interface IPhoto
{
    void Display(string user);
    void Remove(string user);
}
 
// Heavy real subject
public class HighResPhoto : IPhoto
{
    private readonly string _path;
    public HighResPhoto(string path)
    {
        _path = path;
        Console.WriteLine($"  (slow) Loading {_path}...");
    }
    public void Display(string user) => Console.WriteLine($"  Showing {_path}");
    public void Remove(string user)  => Console.WriteLine($"  {_path} deleted");
}
 
// The watchman
public class PhotoProxy : IPhoto
{
    private readonly string _path;
    private readonly HashSet<string> _admins;
    private HighResPhoto? _real;                       // lazy: null until needed
 
    public PhotoProxy(string path, IEnumerable<string> admins)
        => (_path, _admins) = (path, new HashSet<string>(admins));
 
    private HighResPhoto Real => _real ??= new HighResPhoto(_path);
 
    public void Display(string user)
    {
        Console.WriteLine($"[Watchman] {user} viewing {_path}. Logged.");
        Real.Display(user);                            // load on first use
    }
 
    public void Remove(string user)
    {
        if (!_admins.Contains(user))
        {
            Console.WriteLine($"[Watchman] DENIED for {user}.");
            return;                                    // owner never disturbed
        }
        Real.Remove(user);
    }
}
 
// Client
IPhoto photo = new PhotoProxy("annual-day-1.jpg", new[] { "principal" });
photo.Display("riya");        // loads now, then shows
photo.Remove("riya");         // denied at the gate
photo.Remove("principal");    // allowed

The ??= operator makes lazy initialization a one-liner: create the real subject only if _real is still null. (In a multi-threaded server, reach for Lazy<HighResPhoto> instead — see the College corner above.)

🐍 A Python flavour: the school internet watchman

The same watchman guards a different gate in Riya's school: the internet connection in the computer lab. The lab's proxy blocks game sites and caches pages so thirty students opening the same syllabus page do not fetch it thirty times:

import time
 
class WebSite:
    """Subject interface in spirit: open(url) -> page text."""
    def open(self, url):
        raise NotImplementedError
 
class RealWebSite(WebSite):
    """RealSubject: actually fetches over the network. Slow."""
    def open(self, url):
        print(f"  (slow) Fetching {url} over the network...")
        time.sleep(1)                        # pretend network delay
        return f"<html>content of {url}</html>"
 
class SchoolInternetProxy(WebSite):
    """Watchman: blocklist (protection) + page cache (caching)."""
    BLOCKLIST = {"gamezone.com", "timepass.io"}
 
    def __init__(self):
        self._real = RealWebSite()           # could itself be lazy
        self._cache = {}                     # url -> page
 
    def open(self, url):
        if url in self.BLOCKLIST:
            print(f"[Watchman] DENIED: {url} is blocked in school.")
            return None                      # real site never contacted
        if url in self._cache:
            print(f"[Watchman] {url} served from cache, instant.")
            return self._cache[url]
        page = self._real.open(url)          # first time: pay the cost
        self._cache[url] = page              # remember for next student
        return page
 
lab = SchoolInternetProxy()
lab.open("gamezone.com")                     # blocked at the gate
lab.open("ncert.nic.in/syllabus")            # slow fetch, then cached
lab.open("ncert.nic.in/syllabus")            # instant, from cache
[Watchman] DENIED: gamezone.com is blocked in school.
  (slow) Fetching ncert.nic.in/syllabus over the network...
[Watchman] ncert.nic.in/syllabus served from cache, instant.

Three lines of output, three proxy duties: refuse, fetch-once, serve-from-register. Real corporate and school networks run exactly this — they even call the machine "the proxy server."

Where you see it in real software

Proxies are one of the most heavily used patterns in real systems — often generated for you by frameworks.

  • JavaScript's built-in Proxy object. The language itself ships the pattern: new Proxy(target, handler) lets you intercept property reads, writes, and function calls on any object. Vue 3's entire reactivity system is built on it — reading a property through the proxy records a dependency; writing through it triggers UI updates.
  • ORM lazy loading. Hibernate (Java) and Entity Framework (C#) hand you proxy objects instead of real database entities. Ask for an Order and its customer field holds a proxy; the SQL query for the customer fires only when you actually touch order.customer.name. That is a virtual + remote proxy generated at runtime.
  • API gateways. Kong, AWS API Gateway, and similar products are proxies at internet scale: every request passes through them for authentication, rate limiting, logging, and routing before reaching the real backend services. Same interface to callers (HTTP), full gate control behind it.
  • Virtual scrolling and image lazy-loading. Photo apps and infinite feeds show cheap placeholders and load the heavy media only when an item scrolls into view — exactly our gallery example, shipped in every social app you use.
  • RPC stubs and gRPC clients. The client object you call locally is a remote proxy: it serializes your arguments, sends them over the network, and deserializes the reply, while your code looks like a plain method call.
  • Open-source reference. The java-design-patterns proxy example features a wizard tower that admits only three wizards — a tiny protection proxy you can run yourself.

🧭 When to use it and when not to

SituationUse Proxy?
A heavy object should be created only when truly needed (many created, few used)✅ Yes — virtual proxy
You need permission checks without polluting the real class✅ Yes — protection proxy
The real object lives in another process/machine✅ Yes — remote proxy
You want logging, metrics, or caching around calls without editing the subject✅ Yes — smart proxy
The object is cheap to build and open to all❌ No — the proxy is pure ceremony
You want to give the object a simpler, different interface❌ No — that is Facade
The client should consciously stack multiple add-on behaviours❌ No — that is Decorator
Hidden first-call delays would be unacceptable (hard real-time paths)❌ Careful — lazy loading hides when the slow work happens

The same decision as a map. Heavy objects that need gate control sit top-right:

Figure 8: Should you put a watchman in front?

Notice the two half-fits. A cheap object that still needs permission checks (top-left) wants only the protection half of the pattern. A heavy object open to everyone (bottom-right) wants only the virtual half. The full watchman — checking and lazy-loading and logging — earns his chair when both axes are high.

Common mistakes students make

⚠️

Mistake 1: Different interface on the proxy. If clients must call proxy.loadAndDisplay() instead of the subject's display(), substitutability dies — code written for the real object cannot accept the proxy. Same interface is the pattern's first law.

Mistake 2: Forgetting to cache the lazy object. Writing new HighResPhoto(path) inside display() without the null check reloads the photo on every single view. Build once, store, reuse.

Mistake 3: Business logic creeping into the proxy. The watchman checks names; he does not sign deals. If your proxy starts resizing photos or computing prices, that work belongs in the real subject (or a decorator), not in the gate.

Mistake 4: Stale caches. A caching proxy that never invalidates will happily serve yesterday's data after the real subject changed. Decide an invalidation rule the day you add the cache, not after the first bug report.

Mistake 5: Debugging blindness. Because proxies are invisible to the client, "why is this call slow/denied/stale?" puzzles often end at a forgotten proxy. When behaviour seems haunted, ask first: is something wrapping this object?

Compare with cousins

Proxy, Decorator, and Adapter are the three famous wrappers. Facade joins as the fourth lookalike. Sort them once, remember forever:

QuestionProxyDecoratorAdapterFacade
Interface to clientSame as subjectSame as subjectDifferent (converted)New, simpler, over many objects
Main intentControl access (lazy, permissions, cache, remote)Add behaviourMake incompatible things fitSimplify a subsystem
Who manages the wrapper?Framework/wiring — client often unawareThe client, deliberatelyIntegration codeApplication architecture
Stacked in layers?Usually singleYes, by designUsually singleUsually single
One-word memory hookGatekeeperToppingPlug converterFront desk
Figure 9: Four wrappers, four intentions

The Proxy–Decorator pair deserves one extra sentence because their structure is identical: both implement the subject's interface and hold a reference to it. The difference is intent and control. A decorator's client says, "wrap my object in cheese, then butter" — it builds the stack knowingly, to enhance. A proxy is placed in front of the subject by the system, often creating and owning the subject itself, to guard and manage it. Topping versus gatekeeper.

Everything in this article, hung on one tree:

Figure 10: The whole Proxy pattern as a mind map

Quick revision box

+--------------------------------------------------------------+
|                    PROXY — QUICK REVISION                     |
+--------------------------------------------------------------+
| Idea      : A stand-in object with the SAME interface that   |
|             controls access to the real object.              |
| Nickname  : Surrogate                                        |
| Analogy   : Watchman at the gate — checks your name, logs    |
|             your visit, may refuse, owner stays undisturbed. |
| Motto     : Same interface, controlled access.               |
| 4 types   : Virtual (lazy load) | Protection (permissions)   |
|             Remote (other machine) | Smart (cache/log/count) |
| Key moves : real = null at start; build on first use; check  |
|             access BEFORE delegating; cache results.         |
| Lifecycle : NotLoaded -> Loading (once) -> Cached -> Cached  |
| Wins      : Instant startup, clean subject class, security   |
|             and logging without touching business logic.     |
| Watch out : First call can be surprisingly slow; stale       |
|             caches; do not change the interface!             |
| Cousins   : Decorator adds (client stacks it);               |
|             Facade simplifies many; Adapter converts one.    |
| Real life : JS Proxy object (Vue 3), Hibernate/EF lazy       |
|             entities, API gateways, gRPC client stubs        |
+--------------------------------------------------------------+

Practice exercise 📝

  1. ATM card proxy. Your bank account is the real subject with withdraw(amount) and checkBalance(). Build an AtmCard proxy that (a) blocks any single withdrawal above Rs. 10,000 (access control), (b) connects to the account only on first use (lazy loading), and (c) prints a mini-statement line for every operation (logging). The client must use only the shared IAccount interface.
  2. Internet watchman, extended. Take the Python SchoolInternetProxy above and add (a) a per-student visit counter that prints a warning after 20 page opens, and (b) cache invalidation: any cached page older than 60 seconds must be fetched again. Show output proving both behaviours.
  3. Challenge — proxy or decorator? Take your AtmCard from exercise 1 and add an SmsAlertDecorator that sends an SMS message after every successful withdrawal, wrapped around the proxy. Write three sentences on why the SMS wrapper is a decorator but the card itself is a proxy, even though both wrap the same interface.
  4. College challenge — draw the lifecycle. For exercise 1, draw the NotLoaded → Loading → Cached state machine of the bank connection behind the card, and mark on the diagram exactly where each of the three duties (block, lazy connect, log) fires. Then write two sentences on what changes if two threads use the same card at once.

Frequently asked questions

What is the Proxy pattern in simple words?
Proxy puts a stand-in object in front of a real object. The stand-in has the same interface, so clients cannot tell the difference, but it can check permissions, delay creation, cache answers, or log calls before passing work to the real object.
What are the main types of proxy?
Virtual proxy (lazy loading of an expensive object), protection proxy (access control), remote proxy (real object lives on another machine), and smart proxies for caching, logging, or reference counting.
How is Proxy different from Decorator if both wrap objects?
Same structure, different intent. Decorator adds behaviour and is openly stacked by the client. Proxy controls access — lazy creation, permissions, caching — and usually manages the real object's lifecycle without the client knowing.
What is lazy loading in a proxy?
The proxy delays creating the heavy real object until someone actually calls a method that needs it. Building a thousand proxies is nearly free; only the few that get used create their real objects.
Where do I see proxies in real software?
JavaScript's built-in Proxy object, Hibernate and other ORMs lazy-loading database entities, API gateways doing authentication and rate limiting, and virtual scrolling that loads images only when they come on screen.

Further reading

Related Lessons