We close the parade of structural patterns with the last wrapper: an object that implements the same interface as another and passes itself off as it before clients... not to translate (Adapter) or to add responsibilities (Decorator), but to control access to the real object. Sometimes the control is about economy (not loading a 3 MB photo until someone looks at it), sometimes about security (an operator cannot cancel orders outside their zone), sometimes about distance (the real object lives on another server), sometimes about an elephant's memory (remembering answers to avoid repeat trips). In PideYa there is a queue for this pattern: the menu is slow to open because it loads every photo, and the admin panel needs fine-grained permissions now.

Contents

  1. Intent and structure of the pattern
  2. Virtual proxy: the dish photos
  3. Protection proxy: operators and administrators
  4. Caching, logging, and remote proxies
  5. Dynamic proxies: java.lang.reflect.Proxy
  6. Proxy versus Decorator
  7. When to use it and when not to
  8. Common mistakes, exercises, and conclusion

Intent and structure of the pattern

Intent (GoF): provide a surrogate or placeholder for another object to control access to it.

The structure is a wrapper with pedigree: proxy and real object implement the same interface (Subject), so the client cannot tell —and must not be able to tell— which of the two it is talking to. The proxy holds a reference to the real object (or the means to obtain it, which is the whole point of the virtual proxy) and decides on every call whether, when, and how to delegate it.

classDiagram
    class DishImage {
        <<interface>>
        +dimensions() Dimensions
        +paint(canvas: Canvas)
    }
    class RealDishImage {
        -pixels: byte[]
        +dimensions() Dimensions
        +paint(canvas: Canvas)
    }
    class DishImageProxy {
        -url: String
        -knownDimensions: Dimensions
        -real: RealDishImage
        +dimensions() Dimensions
        +paint(canvas: Canvas)
    }
    class ClientApp

    DishImage <|.. RealDishImage
    DishImage <|.. DishImageProxy
    DishImageProxy o-- RealDishImage : creates on demand
    ClientApp --> DishImage : uses without distinguishing
GoF role In PideYa
Subject (common interface) DishImage
RealSubject (the true object, expensive or sensitive) RealDishImage
Proxy (the controlling stand-in) DishImageProxy
Client The app displaying the menu

Under this single structure live several usage patterns with names of their own; the lesson covers the four that matter in practice:

Proxy type What it controls In PideYa
Virtual The when: postpones creating/loading the expensive object until first use Dish photos
Protection The who: checks permissions before delegating Admin panel
Caching The how many times: remembers answers to expensive operations Catalog queries
Remote The where: locally represents an object from another process/machine Service clients (mention)

Virtual proxy: the dish photos

The measured problem: La Bella Napoli's menu has 60 dishes with photos (~3 MB each at the source). Loading them all when the menu opens means 180 MB and eight seconds of waiting... so that the average user sees a screen and a half (about 8 photos). The real object is expensive; the usage, sparse: virtual proxy territory.

public interface DishImage {
    Dimensions dimensions();
    void paint(Canvas canvas);
}

/** The expensive object: downloads and decodes the photo ON CONSTRUCTION. */
public class RealDishImage implements DishImage {

    private final byte[] pixels;
    private final Dimensions dimensions;

    public RealDishImage(String url) {
        this.pixels = Network.download(url);          // 3 MB, hundreds of ms
        this.dimensions = Decoder.measure(pixels);
    }

    @Override public Dimensions dimensions() { return dimensions; }
    @Override public void paint(Canvas canvas) { canvas.paintPixels(pixels); }
}

/** The proxy: cheap to create; only materializes the real one when truly needed. */
public class DishImageProxy implements DishImage {

    private final String url;
    private final Dimensions knownDimensions;         // they come from the catalog: free!
    private RealDishImage real;                       // null until the first paint()

    public DishImageProxy(String url, Dimensions knownDimensions) {
        this.url = url;
        this.knownDimensions = knownDimensions;
    }

    /** Answers WITHOUT touching the real object: laying out the menu downloads nothing. */
    @Override
    public Dimensions dimensions() { return knownDimensions; }

    /** Lazy loading: the first paint pays for the download; the following ones don't. */
    @Override
    public void paint(Canvas canvas) {
        if (real == null) {
            real = new RealDishImage(url);
        }
        real.paint(canvas);
    }
}

The menu now builds 60 proxies (60 tiny objects, zero network) and the app does its layout with dimensions() without downloading a byte; only when a dish scrolls into view does its paint() materialize the real image. Instant opening, and you only pay for what you look at. Two quality nuances:

  • The dimensions() move is half the value: a good virtual proxy answers what it can without disturbing the real object, and delegates only the inevitable.
  • The lazy initialization here is the same problem we studied in depth in Singleton: in a multithreaded environment this if (real == null) would need the same discipline (synchronization or a holder) as there; we won't repeat it, but note the connection.

Protection proxy: operators and administrators

Second control: the who. PideYa's internal panel offers order-management operations, and not all of them are for everyone:

public interface OrderManagement {
    Order view(OrderNumber number);                 // operator and administrator
    void refund(OrderNumber number);                // administrator only
    void cancel(OrderNumber number, String reason); // administrator only
}

Instead of sowing if (user.isAdmin()) throughout the real service —mixing security with business, and trusting nobody forgets an if—, the check is concentrated in a protection proxy wrapping the real service:

public class OrderManagementGuardProxy implements OrderManagement {

    private final OrderManagement real;               // the genuine service
    private final AuthenticatedUser user;             // who is at the keyboard

    public OrderManagementGuardProxy(OrderManagement real, AuthenticatedUser user) {
        this.real = real;
        this.user = user;
    }

    @Override
    public Order view(OrderNumber number) {
        return real.view(number);                     // allowed for every panel role
    }

    @Override
    public void refund(OrderNumber number) {
        require(Role.ADMIN, "refund " + number);
        real.refund(number);
    }

    @Override
    public void cancel(OrderNumber number, String reason) {
        require(Role.ADMIN, "cancel " + number);
        real.cancel(number, reason);
    }

    private void require(Role role, String operation) {
        if (!user.has(role)) {
            EventLog.INSTANCE.warn(user.getId() + " lacks permission for " + operation);
            throw new AccessDeniedException(operation);
        }
    }
}

The composition root hands each panel session the service already wrapped according to its user; the screens program against OrderManagement without knowing whether they are talking to the real thing or the guard. The real service stays clean of security (SRP), the policy lives in one auditable place, and removing or tightening permissions touches neither the service nor the screens.

Caching, logging, and remote proxies

Caching proxy — controls the how many times. The menu query against the catalog service takes ~200 ms and thousands of customers request it; the menu changes a few times a day:

public class CatalogCacheProxy implements Catalog {

    private final Catalog real;
    private final Map<RestaurantId, MenuComponent> cache = new ConcurrentHashMap<>();

    public CatalogCacheProxy(Catalog real) { this.real = real; }

    @Override
    public MenuComponent menuOf(RestaurantId id) {
        return cache.computeIfAbsent(id, real::menuOf);   // the expensive trip, once
    }

    /** The delicate point of every cache: invalidate when the restaurant edits its menu. */
    public void invalidate(RestaurantId id) { cache.remove(id); }
}

(It returns the Composite tree of the menu, by the way: the module's patterns already live side by side.) Don't confuse this proxy with the Flyweight factory: that one shared immutable objects to save memory; this one remembers results of expensive calls to save time, and carries the classic burden of invalidation.

Logging proxy: records every call (who, what, how long it took) before/after delegating — mechanically identical to the LoggingNotifier of the Decorator lesson, and that overlap is no accident: we dissect it in the head-to-head section.

Remote proxy (mention only, developed in the distributed systems module): the real object lives in another process or machine, and the local proxy offers its same interface while hiding network, serialization, and transport errors. It is the pattern behind RMI, gRPC stubs, and generated REST API clients: when PideYa's delivery service splits into its own deployment, the DeliveryService the other pieces keep seeing will be a remote proxy.

Dynamic proxies: java.lang.reflect.Proxy

Handwriting the logging proxy for OrderManagement, and for Catalog, and for PaymentGateway... is repeating the same delegation in a different uniform. Java ships the solution: generate the proxy at runtime for any interface, with the cross-cutting logic written once in an InvocationHandler:

public class LoggingHandler implements InvocationHandler {

    private final Object real;

    public LoggingHandler(Object real) { this.real = real; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.nanoTime();
        try {
            Object result = method.invoke(real, args);       // delegate to the real object
            EventLog.INSTANCE.info(method.getName() + " OK in "
                    + (System.nanoTime() - start) / 1_000_000 + " ms");
            return result;
        } catch (InvocationTargetException e) {
            EventLog.INSTANCE.error(method.getName() + " failed: " + e.getCause());
            throw e.getCause();                              // re-throw the original exception
        }
    }
}
// A logging wrapper for ANY interface, without writing a single proxy class:
OrderManagement logged = (OrderManagement) Proxy.newProxyInstance(
        OrderManagement.class.getClassLoader(),
        new Class<?>[] { OrderManagement.class },
        new LoggingHandler(realManagement));

Catalog loggedCatalog = (Catalog) Proxy.newProxyInstance(
        Catalog.class.getClassLoader(),
        new Class<?>[] { Catalog.class },
        new LoggingHandler(realCatalog));

Proxy.newProxyInstance manufactures on the fly a class that implements the requested interface and funnels all calls into invoke(...). You lose fine-grained typing inside the handler (everything is Method and Object[]) in exchange for writing the cross-cutting behavior once for all interfaces.

This mechanism is the engine room of half the Java ecosystem (a mention to connect with what you already use or will use): Spring AOP wraps your beans in dynamic proxies —that is how @Transactional, @Cacheable, and annotation-based security work: a proxy opens the transaction or checks the role before delegating to your method—; the Spring Data repositories that "have no implementation" are dynamic proxies over the interface you declare; Hibernate loads lazy relationships by handing out virtual proxies of your entities; Mockito's mocks are proxies that record and verify calls. When a framework seems to work magic around your methods, look for the proxy: it is almost always there.

Proxy versus Decorator

The module's classic confusion, because the diagram is the same: implement the wrapped object's interface and delegate. The differences lie in the intent and in three practical consequences:

Aspect Decorator Proxy
Intent Add visible responsibilities to the object Control access to the object
Can it not delegate? No: it always calls the wrapped object (adds around it) Yes: it can refuse (protection), answer on its own (cache, dimensions()), or postpone (virtual)
Who composes? The client, stacking layers at will (the combinatorics is the point) The infrastructure (composition root, framework); the client doesn't even know there's a proxy
Who knows/creates the wrapped object? Receives one already created, any of the interface Often knows how to create or obtain its real object (URL, remote lookup)
Typical cardinality Many layers stackable in any order One proxy (or a few, fixed) in front of the real one

The honest borderline case: a logging wrapper. Was LoggingNotifier a Decorator and LoggingHandler a Proxy? The mechanics are identical; the correct reading is by who decides and for what: if it is an optional layer the developer stacks among others (added responsibility), call it Decorator; if it is a control the infrastructure interposes as a matter of course and the client ignores (watched/measured access), call it Proxy. At the border, the name matters less than understanding both forces — and that is how we will treat it in the final comparison.

When to use it and when not to

Use it when:

  • An object is expensive (memory, network, startup) and often never gets used: virtual proxy (photos, lazy entities, connections).
  • Access must be watched or restricted depending on the caller: protection proxy (panel roles).
  • Repeated, expensive operations with stable answers: caching proxy (catalog), accepting the duty to invalidate.
  • The real object is somewhere else: remote proxy.
  • You need behavior cross-cutting many interfaces (traces, metrics, transactions): dynamic proxies, or the framework that already uses them for you.

Don't use it when:

  • There is nothing to control: a proxy "for architecture's sake" in front of every service is bureaucracy — indirection everyone traverses and nobody benefits from.
  • What you want is to add composable, visible behavior: that's Decorator; or to translate a foreign interface: Adapter; or to simplify many calls: Facade.
  • Transparency would be a dangerous lie: if the remote proxy can take 30 seconds or fail on the network, pretending it is a cheap local call invites mistakes; sometimes it is healthier for the interface to show the asynchrony or the possible failure in its contract.

Relationship to other patterns (mentions only): it shares its silhouette with Decorator and Adapter — the four-way face-off of the wrappers arrives in the next lesson; the virtual proxy's lazy loading reuses Singleton's initialization techniques; the factories from module 2 are the natural place to decide whether the client receives the real object or its proxy — which is exactly how DI containers do it.

Common Mistakes and Tips

  • Business logic in the proxy. If OrderManagementGuardProxy starts deciding how much to refund, it has invaded the business. The proxy decides about access (whether/when/who); the what always belongs to the real object.
  • A cache without invalidation. The caching proxy that never invalidates serves yesterday's menus. Design the invalidation (edit event, TTL) in the same commit as the cache, or you will own the quarter's hardest-to-reproduce bug.
  • A virtual proxy with treacherous identity. equals/hashCode/toString on the unmaterialized proxy can lie or trigger the load by accident (a toString in a log line that downloads 3 MB). Decide what the proxy answers without delegating and document it — the classic pain with Hibernate's lazy entities.
  • Concurrency forgotten in the lazy load. Two threads, two downloaded RealDishImages. You already know the Singleton arsenal; apply it as the case demands (does loading twice matter? sometimes not).
  • Swallowing the denial. A protection proxy that, on denied access, returns null or does nothing turns security into a mystery ("I press refund and nothing happens"). Denying means throwing and logging, as we did, so the failure is visible and auditable.
  • Tip: keep the proxy faithful to the interface contract: same documented exceptions, same semantics. The day the client needs to know whether there is a proxy in front, transparency —the pattern's central asset— is gone; if that day comes, rethink the contract instead of piling up exceptions to the rule.

Exercises

Exercise 1: a read-only proxy for couriers

Couriers see the orders they carry through OrderManagement, but they must only be able to view: any other operation is a programming error in the courier app that must be caught loudly. Write ReadOnlyProxy and state how its policy differs from the role-based protection proxy's.

Exercise 2: reading a dynamic proxy

Using the lesson's LoggingHandler, a colleague writes:

OrderManagement g = (OrderManagement) Proxy.newProxyInstance(
        OrderManagement.class.getClassLoader(),
        new Class<?>[] { OrderManagement.class },
        new LoggingHandler(new OrderManagementGuardProxy(realManagement, user)));

(a) What gets logged when an operator without the administrator role calls g.refund(n): the denial, the attempt, both? (b) What would change if the wrapper order were inverted? (c) Which order do you prefer for a security audit?

Exercise 3: classifying controls

For each need, say which proxy type applies (or whether the right pattern is a different one): (1) a courier's enormous GPS position history should only be fetched from the database if the panel opens its tab; (2) translating OrderManagement calls to an external logistics partner's SDK; (3) calls to the geocoding service (paid, per request) should not repeat for already-resolved addresses; (4) the restaurant app must call the kitchen ticket service that now lives in another deployment.

Solutions

Solution 1:

public class ReadOnlyProxy implements OrderManagement {

    private final OrderManagement real;

    public ReadOnlyProxy(OrderManagement real) { this.real = real; }

    @Override
    public Order view(OrderNumber number) { return real.view(number); }

    @Override
    public void refund(OrderNumber number) {
        throw new UnsupportedOperationException("The courier app is view-only");
    }

    @Override
    public void cancel(OrderNumber number, String reason) {
        throw new UnsupportedOperationException("The courier app is view-only");
    }
}

The policy difference: the role-based proxy decides per user at runtime (the same wrapped service serves different roles; a denial is a security event that gets logged); this one decides per channel, unconditionally (that app is never entitled to write; an attempt betrays a client bug, hence UnsupportedOperationException instead of AccessDeniedException). Same skeleton, different contracts.

Solution 2: (a) Both as one event: the call enters through the logging proxy (which measures), it delegates to the protection one, which denies by throwing AccessDeniedException; the exception travels back through the logging layer, which records "refund failed: AccessDeniedException". The attempt is logged along with its denial. (b) Inverted (protection on the outside), a denied call never reaches the logging: it is rejected earlier, and the log only contains permitted calls. (c) For a security audit, the order in the statement (logging on the outside): denied attempts are precisely what an auditor wants to see. The general moral: wrapper order is semantics, not aesthetics — we saw it with the decorators and it holds again here.

Solution 3: (1) Virtual proxy: an expensive object (huge history) materialized only on first access. (2) No proxy at all: it's Adapter — you need to translate between two different interfaces, not control access behind the same one. (3) Caching proxy, with relaxed invalidation (resolved addresses effectively never expire): direct savings on the bill. (4) Remote proxy: same KitchenTicketService interface, transport hidden — with the contract honesty we discussed (network delays and failures exist).

Conclusion

Proxy completes the wrappers with the intent of control: the same interface as the real object and decision power over every call — postponing it (virtual), refusing it (protection), answering it from memory (cache), crossing the network for you (remote), or measuring it (logging). PideYa now has the DishImageProxy that made the menu instant, the OrderManagementGuardProxy that concentrated the panel's permissions, and the CatalogCacheProxy over the menu tree; and with java.lang.reflect.Proxy you saw the industrial version of the pattern, the one holding up Spring, Hibernate, and Mockito. The fine border with Decorator is also drawn: adding versus controlling, a client that stacks versus an infrastructure that interposes.

And with this one there are seven: adapt, bridge, compose into trees, decorate, simplify, share, and control. Seven structural answers that now ask for the same thing the creational ones asked for at the end of their module: putting them face to face, learning to choose among the look-alikes —those four near-twin wrappers—, seeing how they combine, and reviewing the full map of where each one landed in PideYa. See you in Comparing and Choosing Structural Patterns.

© Copyright 2026. All rights reserved