Home › Low-Level Design › Proxy Pattern
P

Proxy Pattern

A proxy is a stand-in that looks exactly like the real object but gets to run code before, after, or instead of the real call. Same interface, extra brains behind it. You reach for it when you want to add behavior around an object without the caller knowing anything changed. In a 45-minute LLD round, the thing that separates a pass from a "needs more seasoning" is recognizing the shape fast, drawing it, and not fumbling the ten lines of Java.

Low-Level DesignMedium9 min read

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. Same interface, always. The proxy and the real object implement one interface, so a caller holding that interface cannot tell which one it has. Break this and you are not writing a proxy anymore.
  2. It controls access. The proxy decides whether, when, and how the real call happens: delay it, guard it, cache it, log it, or ship it over a wire.
  3. Four flavors. Virtual (lazy load), protection (permission check), remote (network stand-in), smart (caching, logging, ref-counting). Same skeleton, different intercept.
  4. The intercept lives in one place. The real subject stays clean; the cross-cutting concern lives in the proxy. That is why a new method cannot silently skip the guard.
  5. The classic gotcha: it is not Decorator. Proxy controls access to an object it often creates. Decorator adds behavior to an object it was handed. Nearly identical diagram, different intent.

1The problem it solves#

Start with the pain, because the pattern is a reaction to two very specific flavors of it.

Flavor one: the expensive object you may never use.

  • A service loads something heavy: a high-resolution image, a report that takes two seconds, a remote object on another machine.
  • The naive code creates it eagerly and uses it directly:
Image img = new HighResImage("photo.raw"); // loads 40MB from disk NOW
render(img); // but maybe we never even draw it
  • That load happens whether or not anyone looks at the image.
  • Multiply it across a gallery of 500 thumbnails at 40MB each and you are asking for 20GB of disk reads on startup.
  • The app freezes, and most of those bytes are never drawn.

Flavor two: the guard that keeps getting forgotten.

  • You have a BankService and you want every call checked against the user's role.
  • You could sprinkle permission checks inside every method. Now auth logic is tangled into business logic.
  • The next engineer who adds a method forgets the check.
  • That forgotten check is exactly the kind of gap that turns into a security incident.

Both problems share one shape.

  • The caller wants to talk to object X.
  • But something needs to happen around that conversation: delay it, guard it, cache it, log it, or forward it across a network.
  • You do not want to rewrite the caller, and you do not want to pollute X itself.
  • That "something in the middle that pretends to be X" is the proxy.

2The idea (mental model)#

Think of a receptionist for a busy executive.

  • You call the executive's number. A receptionist picks up. From your side, you reached the executive.
  • The receptionist decides whether to let the call through, takes a message if the executive is out, or handles a routine question entirely.
  • You and the executive both talk through the same "interface": a phone call. That is the whole trick.

What that maps to in code.

  • The proxy implements the exact same interface as the real object.
  • It holds a reference to the real object.
  • It intercepts every call and decides what to do before forwarding.
The one constraint that defines the pattern

A proxy does not change the contract. A caller holding the interface cannot tell whether it has the real thing or the proxy. That is Liskov substitution in practice: the proxy must be usable anywhere the real subject is. The moment your "proxy" adds new methods or returns different results, you have drifted into Decorator or Adapter territory.

Four common jobs for a proxy:

  • Virtual proxy: delay creating the expensive object until it is actually needed (lazy loading).
  • Protection proxy: check permissions before forwarding the call.
  • Remote proxy: represent an object that lives in another process or machine, hiding the network details.
  • Smart proxy (caching, logging): do bookkeeping like caching results, counting references, or logging around the call.

3Structure#

Three players, and that is the whole cast.

  • Subject: the interface that both the real object and the proxy implement.
  • RealSubject: the object that does the actual work.
  • Proxy: holds a reference to the real subject and forwards to it after doing its own thing.
  • The caller depends only on the interface. That is the Dependency Inversion Principle, and it is what makes the swap invisible.
classDiagram class Image { <> +display() void } class HighResImage { -String fileName -byte[] data +display() void -loadFromDisk() void } class ImageProxy { -String fileName -HighResImage real +display() void } class Gallery { +show() void } Image <|.. HighResImage Image <|.. ImageProxy ImageProxy --> HighResImage Gallery --> Image
Notice Gallery points at Image, the interface, not at either concrete class. ImageProxy holds a HighResImage it creates lazily. The proxy and the real subject are siblings under one interface. That sibling shape is the signature of this pattern, and it is the thing to draw first on the whiteboard.

4Implementation in Java#

Virtual proxy: lazy image loading#

The expensive work is reading the file off disk. The proxy puts that off until the first display() call.

interface Image {
    void display();
}

class HighResImage implements Image {
    private final String fileName;
    private byte[] data;

    HighResImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(); // the slow part
    }

    private void loadFromDisk() {
        System.out.println("Loading " + fileName + " from disk...");
        this.data = new byte[0]; // pretend this is 40MB
    }

    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }
}

class ImageProxy implements Image {
    private final String fileName;
    private HighResImage real; // created on demand

    ImageProxy(String fileName) {
        this.fileName = fileName; // cheap, no disk read
    }

    @Override
    public void display() {
        if (real == null) {
            real = new HighResImage(fileName); // load now, on first use
        }
        real.display();
    }
}

Building the gallery is instant because no file touches the disk until something is drawn:

class Gallery {
    public static void main(String[] args) {
        List<Image> images = List.of(
            new ImageProxy("beach.raw"),
            new ImageProxy("sunset.raw"),
            new ImageProxy("city.raw")
        );
        // Zero disk reads so far.

        images.get(1).display(); // only sunset loads
    }
}

What just happened: three proxies, zero disk reads, until that one display() call pulls exactly one 40MB file. That is the whole win.

Protection proxy: same skeleton, different intercept#

The proxy guards the call instead of delaying it.

interface BankAccount {
    void withdraw(double amount);
}

class RealBankAccount implements BankAccount {
    private double balance = 1000;

    @Override
    public void withdraw(double amount) {
        balance -= amount;
        System.out.println("Withdrew " + amount + ", balance " + balance);
    }
}

class ProtectedAccount implements BankAccount {
    private final BankAccount real;
    private final String role;

    ProtectedAccount(BankAccount real, String role) {
        this.real = real;
        this.role = role;
    }

    @Override
    public void withdraw(double amount) {
        if (!"OWNER".equals(role)) {
            throw new SecurityException("Not allowed to withdraw");
        }
        real.withdraw(amount);
    }
}

Why this reads as a win in the room:

  • Same interface, same method signature. The caller cannot tell the difference, and that is the point.
  • The guard lives in exactly one place now.
  • A twelfth method on BankAccount cannot silently skip the check: the proxy either wraps the call or the method does not exist.
Pause before you read on: the first `display()` call pays the load cost. What does the second call cost?
Reveal the answer
One null check and one method hop, nothing more. The real object already exists after the first call, so `real == null` is false and the proxy forwards straight through. Interviewers like this "first call vs later calls" distinction because it checks whether you understand that the proxy caches the real reference, not just the result.

Here is the call sequence, which makes that split explicit:

sequenceDiagram participant Client participant Proxy as ImageProxy participant Real as HighResImage Client->>Proxy: display() Proxy->>Proxy: real == null? Proxy->>Real: new HighResImage(fileName) Real-->>Proxy: loaded Proxy->>Real: display() Real-->>Client: shown Client->>Proxy: display() again Proxy->>Real: display() Real-->>Client: shown (no reload)
The first call pays the load once; every later call is a straight forward with no reload. The branch at real == null? is the entire pattern, and the lower half of the diagram is why lazy loading is cheap after the first touch.

5A real-world example#

You have used proxies all day whether you noticed or not.

Spring @Transactional and @Cacheable are proxies.

  • Annotate a bean and Spring wraps it in a dynamic proxy (CGLIB subclass or a JDK interface proxy).
  • Your code calls service.save(), but the proxy opens a transaction, calls the real method, then commits or rolls back.
  • That is a smart proxy doing bookkeeping around the real call.
  • Classic gotcha to drop: a self-invocation inside the same bean skips the proxy, because the call never leaves the object, so the annotation does nothing.

Hibernate lazy loading is a virtual proxy.

  • Fetch an Order and its customer field is often a proxy object.
  • The real customer row is not read from the database until you call order.getCustomer().getName().
  • Same idea as the image gallery, applied to database rows.
  • It is also the source of the infamous LazyInitializationException when the session closes before you touch the proxy.

gRPC and old-school RMI stubs are remote proxies.

  • On the client you call a plain method.
  • The stub serializes your arguments, ships them over the wire, waits for the response, and hands it back as if the object lived next door.

Where it earns its place in LLD prompts:

  • A rate limiter in front of an API client.
  • A permission layer in a document editing system.
  • A caching layer in front of a slow data source.
  • In each one you keep the real object clean and put the cross-cutting concern in a proxy.

6When to use it, and when not to#

Use it when:

  • Creating or using the real object is expensive and you want to defer or cache that work.
  • You need an access control or auditing layer and you want the real object to stay unaware of it.
  • The real object lives somewhere else (another process, a remote host) and you want local code to treat it as if it were here.
  • You want to add behavior around an object but cannot or should not modify its class.

Skip it when:

  • The real object is cheap and always needed. Lazy loading something that costs nothing just adds indirection.
  • You find yourself adding new methods or changing return types. If the interface changes, you want Decorator (adds responsibilities) or Adapter (changes the interface), not Proxy.
  • A simple if check at the call site would do and there is exactly one caller. Do not build three classes to guard one method.

Proxy pays off when the extra behavior must be transparent and reused across many callers. If it is local and one-off, inline it.

7Common mistakes#

Changing the interface.

  • The number one mistake, and the classic misuse.
  • The moment your proxy exposes a method the real object does not have, callers can tell them apart, and you have broken the substitutability that makes the pattern useful.
  • Proxy, Decorator, and Adapter look alike on a diagram. Proxy keeps the same interface and controls access. Decorator keeps the same interface and stacks behavior. Adapter changes the interface. Know which one you are drawing.

Leaking the real object.

  • If your proxy has a getter that hands out the raw RealSubject, a caller can bypass every guard and cache you built.
  • Keep the real reference private.

Forgetting thread safety on the lazy init.

  • The if (real == null) pattern is a race condition under concurrent calls.
  • Two threads can both see null and both create the object, so you load that 40MB file twice and leak a reference.
  • If the object must be a singleton or creation has side effects, synchronize the init or use the holder idiom (or double-checked locking with a volatile field).
  • Saying this out loud in an interview scores points.

Putting business logic in the proxy.

  • The proxy's job is the cross-cutting concern: access, lazy init, caching, logging. The domain work belongs in the real subject.
  • That is the Single Responsibility Principle: the proxy owns "how we get to the object," the real subject owns "what the object does."
  • If your proxy starts computing interest or validating orders, it has grown into something else.

One giant proxy doing four jobs.

  • A proxy that caches and logs and checks permissions and loads lazily is hard to reason about.
  • If you need several concerns, stack several thin proxies, each implementing the interface and wrapping the next.
  • That composition is really Decorator, which is the honest name for it.
Red flag that fails you here

Drawing a "proxy" that exposes extra methods, or one that quietly returns different results than the real object. The interviewer is checking whether you actually understand substitutability. If a caller can tell the proxy and the real object apart, you have not built a proxy, and insisting it is one reads as not knowing the difference from Decorator.

8Where it shows up in interviews#

Interviewers rarely say "use the Proxy pattern." They describe a situation and watch whether you reach for it.

The cues to catch:

  • "Loading the full object is slow, but the list view only needs the name." Virtual proxy.
  • "Only admins can run this operation, and we have a dozen operations." Protection proxy.
  • "The payment service runs in another cluster." Remote proxy.
  • "We call this expensive API a lot and the results do not change often." Caching smart proxy.

Strong answers hit three beats:

  • Name the interface and insist both the proxy and the real object implement it, so the caller never changes.
  • Say exactly what the proxy intercepts and why.
  • Raise one tradeoff on your own: the thread safety of lazy init, or the fact that a proxy adds a layer of indirection that can hide where time is actually spent.
From the interviewer's chair

The follow-up I throw most is "how is this different from a Decorator?" I am not testing memorization. I want to hear a crisp one-liner that shows you know intent drives the choice, not structure. A proxy controls access to an object it often creates or manages. A decorator adds responsibilities to an object it was handed. Nearly identical diagram, the intent is the whole difference.

Follow-ups the interviewer will ask (and your answers)
"How is this different from a Decorator?"
A proxy controls access to an object, often creating or managing it, and keeps the same interface. A decorator adds responsibilities to an object it was handed, also keeping the same interface. The structure is nearly identical; the intent differs. Proxy answers "whether and how you reach the object," decorator answers "what extra behavior wraps the object."
"Your lazy init has `if (real == null)`. Is that safe under concurrency?"
No. Two threads can both see null and both construct the real object, loading the file twice and leaking a reference. Fix it with synchronization, the initialization-on-demand holder idiom, or double-checked locking with a `volatile` field. For a stateless object the race is harmless, but if creation has side effects or must be a singleton, guard it.
"Why not just add the check with an `if` at the call site?"
If there is exactly one caller and one method, an inline `if` is fine and a proxy is overkill. The proxy earns its keep when the concern must be transparent and reused across many callers, and when you want a new method on the subject to be unable to skip the guard. Centralizing it in the proxy is what stops the forgotten-check security gap.
"Spring wraps my bean in a proxy. Why does calling my own annotated method from inside the bean do nothing?"
Self-invocation. The proxy only intercepts calls that come through it from outside. A call from one method of the bean to another stays inside the real object and never crosses the proxy, so `@Transactional` or `@Cacheable` on the inner method is ignored. Fix: call through the injected proxy reference, or split the methods into separate beans.
"If I need caching and logging and a permission check, do I write one big proxy?"
No. One proxy doing four jobs is hard to reason about and violates single responsibility. Stack several thin proxies, each implementing the interface and wrapping the next, in a deliberate order (permission outermost, cache next, logging innermost, for example). At that point you are really composing with Decorator, and naming it honestly is the senior move.

What you can now explain in the room

  • Why a proxy must share the real object's interface, and how that enables an invisible swap.
  • The four flavors (virtual, protection, remote, smart) and which cue signals each one.
  • How to write the ten-line Java skeleton for a lazy proxy and a protection proxy.
  • Why the first call pays the load cost and every later call is a cheap forward.
  • The thread-safety trap in `if (real == null)` and how to close it.
  • The one-liner that separates Proxy from Decorator and Adapter by intent, not shape.

Next, look at Decorator and Adapter so you can tell these three apart on sight, and at the Flyweight pattern for another take on managing expensive objects.

Get a prep plan built around your Proxy Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Proxy Pattern prep plan is on the way.