Proxy

Part of the free Low-Level Design course on LogicWiz, in the chapter: Structural patterns. The class-diagram exercises run in the app.

Proxy: same interface, something in front

"The product page calls the catalogue service thirty times, and each call takes 40 milliseconds. Can we cache it without touching the page?"

Arjun, reading a slow-page alert


The problem: a slow service, and checks in the wrong places

SwiftKart's product data lives in a catalogue service on another machine. RemoteCatalogueService.findProduct(sku) makes a network call that takes about 40 milliseconds. A SKU (stock keeping unit) is the code that identifies one product. The product page shows the product, plus rows of suggestions. That is about thirty lookups, so the page takes over a second to load.

Arjun's first fix put a cache inside the page:

public class ProductPage {
    private final RemoteCatalogueService catalogue;
    private final Map<String, Product> cache = new HashMap<>();

    public ProductPage(RemoteCatalogueService catalogue) {
        this.catalogue = catalogue;
    }

    private Product find(String sku) {
        Product p = cache.get(sku);
        if (p == null) {
            p = catalogue.findProduct(sku); // network call, about 40 ms
            cache.put(sku, p);
        }
        return p;
    }

    // render() calls find() for the product and each suggestion
}

It is faster, but look at the cost:

  • Every page needs the same cache. The search page, the cart and the home page will each copy these lines.
  • The page now has two jobs: showing products and managing a cache.
  • Nothing ever expires. When a price changes, this cache shows the old price until the server restarts.

The admin tools have a similar problem. Only catalogue admins may change a price or remove a product. Today, each admin endpoint starts with its own if (!user.isAdmin()) check, and one new endpoint forgot it.

In both cases, the extra work belongs in front of the service, not inside every caller.

The idea: a receptionist who checks your pass

Think of an office building where you need to meet the director. You do not walk into his room. You stop at the reception desk. The receptionist checks your visitor pass first. If you only ask "Is the director in today?", she answers from her own list without disturbing him. She calls him only when she must.

A proxy works like that receptionist. It is an object with the same interface as a real object, standing in front of it and controlling access to it. The real object is called the real subject. The caller talks to the proxy exactly as it would talk to the real subject, and does not need to know which one it has.

Proxies differ by the job they do in front:

  • A caching proxy keeps recent answers and returns them without calling the real subject.
  • A protection proxy checks whether the caller is allowed, then passes the call on or refuses it.
  • A virtual proxy, also called a lazy-loading proxy, creates an expensive real subject only when it is first needed.
  • A remote proxy looks like a local object but sends each call over the network. Libraries that call other services often generate these for you.

The receptionist picture stops working in one place. You know you are talking to a receptionist, not to the director. A proxy is meant to be invisible. It has the same interface and the same method names, so the caller cannot tell the difference. A receptionist also handles many kinds of requests, while a good proxy does one job.

The structure

classDiagram
  class CatalogueService {
    <<interface>>
    +findProduct(sku) Product
  }
  ProductPage --> CatalogueService : client
  CatalogueService <|.. RemoteCatalogueService
  CatalogueService <|.. CachingCatalogueProxy
  CachingCatalogueProxy --> CatalogueService : real subject

The page points at the interface. Both the real service and the proxy implement it. The proxy holds the real subject through the same interface. In main, you wire them together: new ProductPage(new CachingCatalogueProxy(new RemoteCatalogueService(http), Duration.ofMinutes(5), Clock.systemUTC())). The page never learns that a cache exists.

Build it step by step: three proxies

Step 1: a caching proxy with an expiry.

public class CachingCatalogueProxy implements CatalogueService {
    private record Entry(Product product, Instant storedAt) {}

    private final CatalogueService real;
    private final Duration ttl;
    private final Clock clock;
    private final Map<String, Entry> cache = new ConcurrentHashMap<>();

    public CachingCatalogueProxy(CatalogueService real, Duration ttl, Clock clock) {
        this.real = real;
        this.ttl = ttl;
        this.clock = clock;
    }

    @Override
    public Product findProduct(String sku) {
        Entry e = cache.get(sku);
        if (e != null && e.storedAt().plus(ttl).isAfter(clock.instant())) {
            return e.product();
        }
        Product fresh = real.findProduct(sku);
        cache.put(sku, new Entry(fresh, clock.instant()));
        return fresh;
    }
}

ttl stands for time to live: how long a cached answer is trusted. After five minutes, the proxy asks the real service again, so a price change shows up within five minutes. The Clock is passed in so that tests can move time forward without waiting. The map is a ConcurrentHashMap because many page requests run at the same time; the concurrency chapter explains why a plain HashMap breaks there.

Decide carefully what may be cached. A product page showing a price that is two minutes old is fine. A cached stock count at checkout is not, because you would sell the last packet of butter twice. Checkout should use the real service, without the proxy.

Step 2: a protection proxy for admin operations. AdminCatalogue is an interface with two methods, changePrice and removeProduct. The real class does the work, and the proxy checks the role first.

public class AccessCheckingAdminCatalogue implements AdminCatalogue {
    private final AdminCatalogue real;
    private final CurrentUser user;

    public AccessCheckingAdminCatalogue(AdminCatalogue real, CurrentUser user) {
        this.real = real;
        this.user = user;
    }

    public void changePrice(String sku, long pricePaise) {
        requireRole(Role.CATALOGUE_ADMIN);
        real.changePrice(sku, pricePaise);
    }

    public void removeProduct(String sku) {
        requireRole(Role.CATALOGUE_ADMIN);
        real.removeProduct(sku);
    }

    private void requireRole(Role role) {
        if (!user.hasRole(role)) {
            throw new AccessDeniedException(user.id() + " needs " + role);
        }
    }
}

Every admin endpoint receives this proxy, never the real class. A new endpoint cannot forget the check, because the check is no longer its job.

Step 3: a virtual proxy for the photo gallery. A product's full gallery has twenty large photos, and most customers never open it.

public class LazyGallery implements Gallery {
    private final String sku;
    private final GalleryLoader loader;
    private Gallery real; // stays null until someone opens the gallery

    public LazyGallery(String sku, GalleryLoader loader) {
        this.sku = sku;
        this.loader = loader;
    }

    @Override
    public List<Image> images() {
        if (real == null) {
            real = loader.load(sku); // slow: fetches twenty full-size photos
        }
        return real.images();
    }
}

Galleries nobody opens are never loaded. The first customer to open one waits for the load. This version is not safe if two threads open the same gallery at the same moment; the concurrency chapter shows the fix.

Arjun looked at the three classes. "These are the decorator from two lessons ago. Same interface, the inner object in a field, the call passed on. Why give them another name?" He is right about the shape. The difference is the intent, and it shows in who builds the object and why:

Decorator Proxy
Purpose adds a feature to the result controls access to the real subject
Who builds it the caller, often stacking several the wiring in main; callers do not know
The inner object always passed in from outside sometimes created by the proxy itself
Calls the inner object every time maybe never (a cache hit, a refused user)

💡 Tip: In reviews and interviews, name the intent: "This proxy caches catalogue lookups for five minutes." It says more than the class diagram can.

When to use a proxy, and what it costs

Use a proxy when the real object is slow, far away, expensive to create or sensitive, and you want its callers to stay unchanged.

Do not use one for data that must be fresh at that moment, such as stock at checkout or a wallet balance before a payment. And if a check belongs to only one use case, a line in that one method may be clearer than a new class.

Proxies have costs:

  • Stale data. Every cache must answer "when is this out of date?" Removing or refreshing cached answers when the real data changes is called cache invalidation. A TTL is its simplest form.
  • Hidden waits. Callers come to expect fast calls. The first call, or the call after expiry, still takes 40 milliseconds.
  • A protection proxy guards only the door it stands at. If any class can get the real AdminCatalogue directly, the check can be skipped. Create the real object only inside the wiring code.
  • Memory. A cache with no size limit grows until the server runs out of memory. The LRU cache problem in the library shows how to limit it.

Frameworks use proxies everywhere. Spring wraps a service method marked @Transactional in a proxy that starts and ends a database transaction around each call. Hibernate gives you a proxy for a related row and loads it only when you touch it. Knowing the pattern helps you read their stack traces.

In interview problems, you meet proxies as a cache in front of a slow database and as a rate limiter in front of an API. An image viewer that loads full pictures only when opened is a virtual proxy.

Try it: cache the catalogue

The starter has two pages, each calling the remote catalogue and keeping its own cache. Add the interface, move the cache into a proxy, and make the pages depend only on the interface.

📌 Summary: A proxy has the same interface as the real object and controls access to it: caching, permission checks or lazy creation. Same shape as a decorator, different intent. Decide what may go stale.

What Arjun learns next

These five patterns arrange objects: fitting one to another, putting one door in front of many, wrapping, nesting, and guarding. The next chapter is about behaviour. Ops keeps changing the delivery-fee rules, and Arjun learns to swap a rule while the app runs: the strategy pattern.

Check your understanding

  1. Checkout reads the stock count for the last packet of butter. Should that read go through the caching catalogue proxy?
  2. Most customers never open a product's photo gallery. What does the LazyGallery proxy change?
  3. The protection proxy checks the role, but a new report class is given the real AdminCatalogue object directly. What is the result?
  4. Which statement best separates a proxy from a decorator?