Low-level designCore9 min

Proxy

Put a stand-in in front of an object, with the same interface, to control how calls reach it: from a cache, on first use, after a permission check, or across a network. Callers never know the difference.

Watch it evolve1 / 8
The problem

Screens ask a paid snow service

Each Screen holds a SnowReport and asks it on every refresh. RemoteSnowService implements it with a web request.

80
Start
Stage 1
Final

1 · The situation

Three screens and a snow report

A ski area shows the snow depth on screens at its lifts, on its app's home page and at the ticket desk. The depth comes from a snow-report service run by another company. The code reaches it through an interface, SnowReport, with one method, depthCm(resort), so the screens can be tested with a fake.

RemoteSnowService implements the interface. In this sample it reads a map and counts its calls; the real one makes a web request. Each Screen holds a SnowReport and asks it whenever it refreshes. Python writes the method depth_cm().

The first versionJava
import java.util.HashMap;
import java.util.List;
import java.util.Map;

interface SnowReport {
    int depthCm(String resort);
}

class RemoteSnowService implements SnowReport {
    private final Map<String, Integer> depths = new HashMap<>();
    private int calls;

    void measured(String resort, int cm) {
        depths.put(resort, cm);
    }

    public int depthCm(String resort) {
        calls++;
        return depths.get(resort);
    }

    int calls() {
        return calls;
    }
}

class Screen {
    private final String name;
    private final String resort;
    private final SnowReport report;

    Screen(String name, String resort, SnowReport report) {
        this.name = name;
        this.resort = resort;
        this.report = report;
    }

    String show() {
        return name + ": " + report.depthCm(resort) + " cm at " + resort;
    }
}

public class Main {
    public static void main(String[] args) {
        RemoteSnowService service = new RemoteSnowService();
        service.measured("Pine Ridge", 112);
        service.measured("Elk Basin", 86);
        SnowReport report = service;
        List<Screen> screens = List.of(
                new Screen("Lift board", "Pine Ridge", report),
                new Screen("App home", "Pine Ridge", report),
                new Screen("Ticket desk", "Elk Basin", report));
        for (int minute : new int[] {0, 5, 12}) {
            System.out.println("Minute " + minute);
            if (minute == 5) service.measured("Pine Ridge", 118);
            for (Screen s : screens) System.out.println("  " + s.show());
        }
        System.out.println("Calls to the snow service: " + service.calls());
    }
}
«interface»SnowReport+ depthCm(String): intRemoteSnowService− depths: Map<String, Integer>− calls: int~ measured(String, int)+ depthCm(String): int~ calls(): intScreen− name: String− resort: String− report: SnowReport~ show(): String

The class diagram of this version, drawn from its code: every screen talks to the service through SnowReport.

The program refreshes the screens at minutes 0, 5 and 12. At minute 5, new snow is measured at Pine Ridge:

Minute 0
  Lift board: 112 cm at Pine Ridge
  App home: 112 cm at Pine Ridge
  Ticket desk: 86 cm at Elk Basin
Minute 5
  Lift board: 118 cm at Pine Ridge
  App home: 118 cm at Pine Ridge
  Ticket desk: 86 cm at Elk Basin
Minute 12
  Lift board: 118 cm at Pine Ridge
  App home: 118 cm at Pine Ridge
  Ticket desk: 86 cm at Elk Basin
Calls to the snow service: 9

2 · The change

The service starts charging per call

The provider moves to paid plans: every request is billed, and each plan allows a fixed number of requests an hour. The ski area has about 40 screens that refresh every minute, and the depth changes a few times a day. That's 2,400 calls an hour, nearly all of them returning what the last call returned.

Every refresh is a billed callJava
class RemoteSnowService implements SnowReport {
    private final Map<String, Integer> depths = new HashMap<>();
    private int calls;

    void measured(String resort, int cm) {
        depths.put(resort, cm);
    }

    public int depthCm(String resort) {
        calls++;
        return depths.get(resort);
    }

    int calls() {
        return calls;
    }
}

The answer is a cache, but where it goes matters:

Where the cache goes

What it costs

In each Screen

Every kind of screen repeats the same caching code, and screens showing the same resort still each pay for their own call

In RemoteSnowService

The network client also decides freshness; its tests need a clock, and no caller can get a live reading

Between the screens and the service, behind SnowReport

Neither the screens nor the service change

The screens still need exactly what they had: a SnowReport. What changes is how a request reaches the service, and that's a job for an object that stands between them.

3 · The refactor

Put a stand-in between the screens and the service

Keep the interface and both sides of it. Add a class that implements SnowReport itself and decides when a call goes through:

Clock reports the minutes since the start; main moves it forward here, and a test can too. A cache that reads the system time directly is hard to test.

Java
class Clock {
    private int minutes;

    int minutes() {
        return minutes;
    }

    void advance(int by) {
        minutes += by;
    }
}
«interface»SnowReport+ depthCm(String): intRemoteSnowService− depths: Map<String, Integer>− calls: int~ measured(String, int)+ depthCm(String): int~ calls(): intClock− minutes: int~ minutes(): int~ advance(int)CachedSnowReport− FRESH_MINUTES: int− service: RemoteSnowService− clock: Clock− depths: Map<String, Integer>− fetchedAt: Map<String, Integer>+ depthCm(String): int− isFresh(String): booleanScreen− name: String− resort: String− report: SnowReport~ show(): String

Four calls instead of nine. Look at minute 5, though: the screens answer from the reading taken at minute 0.

Minute 0
  Lift board: 112 cm at Pine Ridge
  App home: 112 cm at Pine Ridge
  Ticket desk: 86 cm at Elk Basin
Minute 5
  Lift board: 112 cm at Pine Ridge
  App home: 112 cm at Pine Ridge
  Ticket desk: 86 cm at Elk Basin
Minute 12
  Lift board: 118 cm at Pine Ridge
  App home: 118 cm at Pine Ridge
  Ticket desk: 86 cm at Elk Basin
Calls to the snow service: 4

Minute 5 shows 112 cm from the minute-0 reading; minute 12 fetches the new 118 cm.

Straight to the service
9
Through the stand-in
4

Calls to the snow service in 12 minutes, for three screens and two resorts.

4 · The pattern

Proxy: a stand-in that controls access

Proxy puts an object in front of another one, with the same interface. Callers talk to the proxy as if it were the real thing, and the proxy decides when and how each call reaches it: now, from a cache, only after a check, or not at all.

«interface»SnowReport+ depthCm(String): intRemoteSnowService− depths: Map<String, Integer>− calls: int~ measured(String, int)+ depthCm(String): int~ calls(): intClock− minutes: int~ minutes(): int~ advance(int)CachedSnowReport− FRESH_MINUTES: int− service: RemoteSnowService− clock: Clock− depths: Map<String, Integer>− fetchedAt: Map<String, Integer>+ depthCm(String): int− isFresh(String): booleanScreen− name: String− resort: String− report: SnowReport~ show(): String

The roles in the ski area's code: SnowReport is the subject, RemoteSnowService the real subject, CachedSnowReport the proxy.

Role

In the ski app

Its job

Subject

SnowReport

The interface both the real object and the proxy implement

Real subject

RemoteSnowService

Does the actual work, and is costly or sensitive to reach

Proxy

CachedSnowReport

Holds the real subject and controls the calls that reach it

Client

Screen

Uses the subject, and never knows which object it has

The first call goes through to the service; the second, five minutes later, is answered by the proxy alone.

Caching is one reason to stand in front of an object. The others give the pattern its usual names:

Kind of proxy

What it controls

Example

Caching

Repeated calls

This story: answer from a recent copy

Virtual

When the real object is created

Load a large document only when someone first reads it

Protection

Who may call

Check the caller's permission, then pass the call on or refuse it

Remote

Where the object lives

A local object that sends each call over the network to the real one

5 · What it costs

Stale answers, and one more hop

  • Stale answers. A caching proxy can be as old as its freshness window: at minute 5 the screens showed 112 cm while the service already had 118.
  • One more class to keep in step. Each new method on SnowReport needs one on the proxy too.
  • Hidden behavior. Callers can't tell they're talking to a proxy. That's the point, and it's also why a slow or stale answer can be hard to trace. Name proxies plainly, and let them report what they do.
  • Memory. The cache keeps an entry for every resort anyone asked about.

Situation

Does a proxy fit?

Repeated reads of slow data that rarely changes

Yes: a caching proxy

Every read must be current, such as a closure warning

No: call the real object

An object that is costly to create and often unused

Yes: a virtual proxy

Calls must be checked against permissions

Yes: a protection proxy

Callers need a different interface

No: that is an Adapter

6 · Getting it right

Same interface, and nothing surprising behind it

  • Implement the same interface exactly. A proxy that adds methods, or changes what one returns, makes callers depend on it.
  • Keep its job to access. Caching, checking, delaying and forwarding are a proxy's work. Adding new behavior to the answer is a decorator's.
  • Make the rules testable. Inject the clock, the permission source or the factory the proxy needs, as CachedSnowReport takes its Clock.
  • Mind shared state. A cache used by several threads at once needs locking, which the concurrency pages cover.
  • Wire it in one place. Only main knows a proxy is there, so removing it is a one-line change.

Each language can also build a proxy without writing one class per interface:

Language

A proxy by hand

Built in

Java

A class that implements the interface

java.lang.reflect.Proxy creates a class at run time for any list of interfaces, and sends every call to one invocation handler

C++

A class deriving from the same abstract class

None for this; each proxy is written by hand

JavaScript

A class with the same methods

The Proxy object wraps another object and intercepts its operations through a handler

Python

A class with the same methods

__getattr__ can forward every attribute the proxy doesn't define to the real object

7 · Look-alikes

Same shape, different intent

Several patterns put one object in front of another. What tells them apart is what they do to the call:

Pattern

What it does to the call

Interface it offers

Proxy

Decides whether, when and how the call reaches the real object

The same as the real object

Decorator

Adds work around the call, and can be stacked

The same as the object it wraps

Adapter

Translates the call for an object with a different interface

The one the client expects

Facade

Turns one simple call into several calls on a subsystem

A new, smaller one

Proxy and Decorator have the same structure, a class holding another object of its own interface, so the difference is intent. A decorator adds to what happens and is given whatever object it wraps. A proxy controls access, and usually knows its real subject: CachedSnowReport holds a RemoteSnowService, not just any SnowReport.

8 · In practice

Proxies you have already used

Proxies are common where a library needs to stand in for objects it doesn't know in advance. Java's java.lang.reflect.Proxy makes an object that implements interfaces chosen at run time and routes every call through an invocation handler, which frameworks use to add transactions or remote calls. JavaScript's Proxy intercepts property reads, writes and calls on any object, through the traps of a handler.

The practice checks recognize a Proxy when a class implements an interface, holds the real object behind it (one it creates, or a concrete type), and controls access before passing a call on: a check, a lazy creation, or a cache. In the snow app they also read Screen as a Strategy context, since it holds an interface with two implementations, so this page doesn't show their card for the final version.

Check yourself

4 questions

1. The avalanche office adds a screen that must always show the live snow depth. How should it read the depth?
2. Why didn't Screen change when the cache was added?
3. Which of these wrappers is a Proxy rather than a Decorator?
4. At minute 5 the screens showed 112 cm, though 118 cm had been measured. What is that?

Takeaways

Remember this

  • A Proxy stands in for an object, with the same interface, and controls how calls reach it.
  • Callers never change: they hold the interface, and only the wiring decides whether a proxy is in front.
  • Caching, lazy creation, permission checks and remote calls are the usual reasons for a proxy.
  • A caching proxy trades freshness for fewer calls; choose the window by what each caller can tolerate.
  • Proxy and Decorator look the same; a proxy controls access, a decorator adds behavior.