← All patterns

Proxy

Structural · view source on GitHub

Read this in: English · Português · Español

Category: Structural

The problem

Accessing an object directly is sometimes expensive, slow, or needs a check applied every time — a network call, a large resource load, a permission check. Making every caller remember to apply that logic themselves (check the cache first, verify permission, delay loading until actually needed) means the logic gets duplicated or forgotten at some call site eventually.

The solution

Introduce a stand-in that implements the exact same interface as the real object, and put the extra logic (caching, lazy loading, access control) inside the stand-in instead of at every call site. Callers hold the proxy and use it exactly like the real thing — they can't tell the difference from the interface alone.

classDiagram
    class Subject {
        <<interface>>
    }
    class RealSubject
    class Proxy {
        -realSubject
    }
    Subject <|.. RealSubject
    Subject <|.. Proxy
    Proxy --> RealSubject : controls access to
    Client --> Subject

Classic example

classic/ImageProxy implements the same Image interface as RealImage, but doesn't construct the (expensive-to-load) real image until the first display() call — the canonical virtual proxy, delaying a costly load until it's actually needed instead of at construction time. ImageProxyTest checks the real image genuinely isn't loaded before the first display() call, and that a second call reuses the same loaded image rather than reloading it.

Applied example: caching an expensive credit-score bureau lookup

applied/CachingCreditScoreProxy implements the same CreditScoreBureau contract as ExternalCreditScoreBureau — a stand-in for a real external bureau call that's slow and, in production, billed per request. A loan-approval flow that calls lookupScore() several times for the same applicant (once at intake, again at underwriting, again at final approval, say) only triggers one real external call; every call after the first is served from the proxy's cache. CachingCreditScoreProxyTest proves this directly by counting real calls on the underlying bureau, and confirms different applicants each still trigger their own real lookup.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :structural:proxy:jacocoTestReport

Report at structural/proxy/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/structural/proxy/classic/ImageProxyTest.java
package com.designpatterns.structural.proxy.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ImageProxyTest {

    @Test
    void theRealImageIsNotLoadedUntilTheFirstDisplayCall() {
        ImageProxy proxy = new ImageProxy("photo.png");

        assertThat(proxy.isLoaded()).isFalse();

        String result = proxy.display();

        assertThat(proxy.isLoaded()).isTrue();
        assertThat(result).isEqualTo("Displaying photo.png");
    }

    @Test
    void repeatedDisplayCallsReuseTheAlreadyLoadedImage() {
        ImageProxy proxy = new ImageProxy("photo.png");

        proxy.display();
        String secondResult = proxy.display();

        assertThat(secondResult).isEqualTo("Displaying photo.png");
        assertThat(proxy.isLoaded()).isTrue();
    }
}
src/test/java/com/designpatterns/structural/proxy/applied/CachingCreditScoreProxyTest.java
package com.designpatterns.structural.proxy.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CachingCreditScoreProxyTest {

    @Test
    void repeatedLookupsForTheSameTaxIdHitTheRealBureauOnlyOnce() {
        ExternalCreditScoreBureau realBureau = new ExternalCreditScoreBureau();
        CachingCreditScoreProxy proxy = new CachingCreditScoreProxy(realBureau);

        int first = proxy.lookupScore("111.111.111-11");
        int second = proxy.lookupScore("111.111.111-11");
        int third = proxy.lookupScore("111.111.111-11");

        assertThat(first).isEqualTo(second).isEqualTo(third);
        assertThat(realBureau.callCount()).isEqualTo(1);
    }

    @Test
    void differentTaxIdsEachTriggerTheirOwnRealBureauCall() {
        ExternalCreditScoreBureau realBureau = new ExternalCreditScoreBureau();
        CachingCreditScoreProxy proxy = new CachingCreditScoreProxy(realBureau);

        proxy.lookupScore("111.111.111-11");
        proxy.lookupScore("222.222.222-22");

        assertThat(realBureau.callCount()).isEqualTo(2);
    }

    @Test
    void theProxyReturnsExactlyWhatTheRealBureauWouldHaveReturned() {
        ExternalCreditScoreBureau realBureau = new ExternalCreditScoreBureau();
        CachingCreditScoreProxy proxy = new CachingCreditScoreProxy(realBureau);
        String taxId = "333.333.333-33";

        int viaProxy = proxy.lookupScore(taxId);
        int direct = realBureau.lookupScore(taxId);

        assertThat(viaProxy).isEqualTo(direct);
    }
}

View full JaCoCo coverage report →