# Proxy

> Put a stand-in with the same interface in front of an object to control access to it: create it lazily, check permissions, or reach it remotely.

- kind: pattern
- category: structural
- aliases: Surrogate, Stand-in
- canonical: https://aipatterns.dev/design-patterns/proxy
- inspired-by: https://refactoring.guru/design-patterns/proxy

## Intent

Give clients an object that implements the same interface as the real one but controls when, whether and how the real one is reached. The proxy can delay creating an expensive subject until first use, refuse calls the caller is not allowed to make, forward calls over a network, or count references. Clients cannot tell the proxy from the subject.

## Forces

- **Expensive setup** vs **fast startup**: loading a 400 MB model at boot slows every deploy, even though most requests never need it.
- **Callers want a plain object** vs **access needs rules**: the code that classifies messages should not also manage loading, locking or permissions.
- **Location transparency** vs **honest failure modes**: a remote object that looks local hides latency and network errors.
- **One access rule** vs **many call sites**: when every caller checks "is it loaded yet" or "is this user allowed", one of them eventually forgets.

## Therefore

**Implement the subject's interface in a proxy class that holds (or knows how to obtain) the real subject, and put the access-control logic in the proxy before delegating each call.**

The proxy usually manages the subject's lifecycle. It creates the subject, or receives a way to create it, rather than being handed a ready instance by the client. That is the practical line between a proxy and a [Decorator](https://aipatterns.dev/design-patterns/decorator.md), which the client builds by wrapping an object it already has.

## Structure

```mermaid
classDiagram
  class InboxFilter {
    -SpamClassifier classifier
    +isSpam(sender, body) boolean
  }
  class SpamClassifier {
    <<interface>>
    +score(message) double
  }
  class ModelClassifier {
    -Map weights
    +score(message) double
  }
  class LazyClassifier {
    -SpamClassifier real
    -Loader load
    +score(message) double
  }
  InboxFilter --> SpamClassifier
  SpamClassifier <|.. ModelClassifier
  SpamClassifier <|.. LazyClassifier
  LazyClassifier --> ModelClassifier : creates on first use
```

### Participants

- **Subject** (`SpamClassifier`): the interface shared by the proxy and the real object.
- **Real subject** (`ModelClassifier`): does the actual work and is expensive, remote or sensitive.
- **Proxy** (`LazyClassifier`): same interface; controls access, creation and lifetime of the real subject.
- **Client** (`InboxFilter`): depends on the subject interface and is unaware which one it holds.

Common kinds of proxy: *virtual* (lazy creation, as in the example), *protection* (permission checks), *remote* (local stand-in for a network object, such as a gRPC stub), and *smart reference* (reference counting, locking, or releasing the subject when idle).

## Use it when

- An object is expensive to create and many code paths never use it.
- Access needs a check (role, tenant, quota) that should be enforced in one place for every caller.
- The real object lives in another process or service and you want callers to use a local interface (RPC client stubs).
- You need to manage a resource's lifetime: open a connection on first use, close it after idle time, count users.
- You need to intercept calls to an object you do not own, without changing its callers.

## Do not use it when

- The object is cheap to create. Construct it eagerly; lazy loading adds locking and makes the first request slow.
- You only need lazy initialization of one field. Use the language's built-in: `functools.cached_property` in Python, `sync.OnceValue` in Go, a memoized `Supplier` or holder class in Java, `??=` in TypeScript.
- You want to intercept every property access on a dynamic object in JavaScript. Use the built-in `Proxy` object instead of a hand-written class. In Python, `__getattr__` or descriptors cover the same ground.
- The interface is wide and the proxy only acts on construction. Hand-forwarding thirty methods invites drift; use a dynamic proxy (`java.lang.reflect.Proxy`), code generation, or a lazy factory instead.
- You are adding optional behavior such as caching, logging or retries that the client stacks freely. That is [Decorator](https://aipatterns.dev/design-patterns/decorator.md).
- The remote call is slow or fallible, and pretending it is local hides that. Expose an explicit async client with timeouts and errors in its signature.

## Application grill

**Inspect first:** the access being controlled (lazy construction, permissions, remote reach); the proxies already in the stack (HTTP clients, API gateways, service meshes, CDNs, the language's `Proxy`, lazy getters); whether the subject is truly remote or just far away in the codebase.

**Missing pieces:** in-process: the language's `Proxy` or a lazy getter - nothing to buy. Remote: reuse the existing mesh, gateway or service client before writing a custom remote proxy; a custom one is a network hop you now own.

**Ask before you build:**

- **Scale:** does the remote proxy add a per-call network hop that the latency budget survives?
- **Performance:** when is the subject actually constructed under lazy loading - does the first caller absorb the stall?
- **Reliability:** when the subject is unreachable, what does the proxy return - timeout, fallback, error?
- **Budget:** is configuring an existing gateway cheaper than hand-rolled proxy code?
- **Implementation:** does the proxy keep the interface identical (the same test suite passes against proxy and subject)?

## Implementation steps

1. Make sure clients depend on an interface. Extract one from the real class if needed ([Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md)).
2. Write the proxy class implementing that interface. Give it what it needs to reach the subject: a loader function, an address, or a reference plus a policy.
3. In each method, apply the control (create if missing, check permission, open connection), then delegate.
4. Make the control thread-safe if the proxy is shared: use a lock, `sync.Once`, or a synchronized accessor.
5. Wire the proxy at the composition root in place of the real subject.
6. Test both paths: the proxy before the subject exists and after, and allowed and denied calls for a protection proxy.

## Example

A mail service scores incoming messages with a spam model that takes several seconds and hundreds of megabytes to load. Most mail comes from trusted senders and never reaches the model, so the service should start without loading it.

```ts title="spam.ts"
interface SpamClassifier {
  score(message: string): number; // 0..1
}

// Real subject: expensive to construct.
class ModelClassifier implements SpamClassifier {
  private readonly weights: Map<string, number>;
  constructor(path: string) {
    console.log(`loading model from ${path} ...`); // stands in for seconds of I/O
    this.weights = new Map([["winner", 0.6], ["crypto", 0.5], ["free", 0.3]]);
  }
  score(message: string): number {
    const words = message.toLowerCase().split(/\W+/);
    return Math.min(1, words.reduce((sum, w) => sum + (this.weights.get(w) ?? 0), 0));
  }
}

// Virtual proxy: same interface, creates the subject on first use.
// Single-threaded runtime, so no lock is needed.
class LazyClassifier implements SpamClassifier {
  private real?: SpamClassifier;
  constructor(private readonly load: () => SpamClassifier) {}
  score(message: string): number {
    this.real ??= this.load();
    return this.real.score(message);
  }
}

class InboxFilter {
  constructor(private readonly classifier: SpamClassifier, private readonly trusted: Set<string>) {}
  isSpam(sender: string, body: string): boolean {
    if (this.trusted.has(sender)) return false; // most mail stops here
    return this.classifier.score(body) >= 0.7;
  }
}

const filter = new InboxFilter(
  new LazyClassifier(() => new ModelClassifier("models/spam-v3.bin")),
  new Set(["billing@acme.io", "ci@acme.io"]),
);
console.log("service ready"); // printed before any model load
console.log(filter.isSpam("billing@acme.io", "Your invoice is ready"));       // false, no load
console.log(filter.isSpam("promo@deals.biz", "WINNER: claim free crypto now")); // loads once, true
console.log(filter.isSpam("friend@mail.com", "Lunch tomorrow?"));              // false, already loaded
```

```python title="spam.py"
import re
import threading
from typing import Callable, Protocol

class SpamClassifier(Protocol):
    def score(self, message: str) -> float: ...

class ModelClassifier:
    def __init__(self, path: str) -> None:
        print(f"loading model from {path} ...")  # stands in for seconds of I/O
        self._weights = {"winner": 0.6, "crypto": 0.5, "free": 0.3}

    def score(self, message: str) -> float:
        words = re.findall(r"\w+", message.lower())
        return min(1.0, sum(self._weights.get(w, 0.0) for w in words))

class LazyClassifier:
    """Virtual proxy. Safe to share across threads."""

    def __init__(self, load: Callable[[], SpamClassifier]) -> None:
        self._load = load
        self._real: SpamClassifier | None = None
        self._lock = threading.Lock()

    def _subject(self) -> SpamClassifier:
        if self._real is None:
            with self._lock:
                if self._real is None:
                    self._real = self._load()
        return self._real

    def score(self, message: str) -> float:
        return self._subject().score(message)

class InboxFilter:
    def __init__(self, classifier: SpamClassifier, trusted: set[str]) -> None:
        self._classifier, self._trusted = classifier, trusted

    def is_spam(self, sender: str, body: str) -> bool:
        if sender in self._trusted:
            return False  # most mail stops here
        return self._classifier.score(body) >= 0.7

if __name__ == "__main__":
    spam_filter = InboxFilter(
        LazyClassifier(lambda: ModelClassifier("models/spam-v3.bin")),
        {"billing@acme.io", "ci@acme.io"},
    )
    print("service ready")
    print(spam_filter.is_spam("billing@acme.io", "Your invoice is ready"))        # False, no load
    print(spam_filter.is_spam("promo@deals.biz", "WINNER: claim free crypto now"))  # loads once, True
    print(spam_filter.is_spam("friend@mail.com", "Lunch tomorrow?"))               # False
```

```go title="spam.go"
package main

import (
	"fmt"
	"strings"
	"sync"
	"unicode"
)

type SpamClassifier interface {
	Score(message string) float64
}

type ModelClassifier struct{ weights map[string]float64 }

func LoadModel(path string) *ModelClassifier {
	fmt.Printf("loading model from %s ...\n", path) // stands in for seconds of I/O
	return &ModelClassifier{weights: map[string]float64{"winner": 0.6, "crypto": 0.5, "free": 0.3}}
}

func (m *ModelClassifier) Score(message string) float64 {
	words := strings.FieldsFunc(strings.ToLower(message), func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsDigit(r)
	})
	sum := 0.0
	for _, w := range words {
		sum += m.weights[w]
	}
	return min(1, sum)
}

// LazyClassifier is a virtual proxy. sync.Once makes it safe for concurrent use.
type LazyClassifier struct {
	once sync.Once
	load func() SpamClassifier
	real SpamClassifier
}

func NewLazyClassifier(load func() SpamClassifier) *LazyClassifier {
	return &LazyClassifier{load: load}
}

func (l *LazyClassifier) Score(message string) float64 {
	l.once.Do(func() { l.real = l.load() })
	return l.real.Score(message)
}

type InboxFilter struct {
	Classifier SpamClassifier
	Trusted    map[string]bool
}

func (f InboxFilter) IsSpam(sender, body string) bool {
	if f.Trusted[sender] {
		return false // most mail stops here
	}
	return f.Classifier.Score(body) >= 0.7
}

func main() {
	filter := InboxFilter{
		Classifier: NewLazyClassifier(func() SpamClassifier { return LoadModel("models/spam-v3.bin") }),
		Trusted:    map[string]bool{"billing@acme.io": true, "ci@acme.io": true},
	}
	fmt.Println("service ready")
	fmt.Println(filter.IsSpam("billing@acme.io", "Your invoice is ready"))         // false, no load
	fmt.Println(filter.IsSpam("promo@deals.biz", "WINNER: claim free crypto now")) // loads once, true
	fmt.Println(filter.IsSpam("friend@mail.com", "Lunch tomorrow?"))              // false
}
```

```java title="Spam.java"
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

public class Spam {
    interface SpamClassifier {
        double score(String message);
    }

    static final class ModelClassifier implements SpamClassifier {
        private final Map<String, Double> weights;

        ModelClassifier(String path) {
            System.out.println("loading model from " + path + " ..."); // stands in for seconds of I/O
            weights = Map.of("winner", 0.6, "crypto", 0.5, "free", 0.3);
        }

        public double score(String message) {
            double sum = 0;
            for (String w : message.toLowerCase().split("\\W+")) sum += weights.getOrDefault(w, 0.0);
            return Math.min(1.0, sum);
        }
    }

    /** Virtual proxy: creates the subject on first use, thread-safe. */
    static final class LazyClassifier implements SpamClassifier {
        private final Supplier<SpamClassifier> loader;
        private SpamClassifier real;

        LazyClassifier(Supplier<SpamClassifier> loader) { this.loader = loader; }

        private synchronized SpamClassifier subject() {
            if (real == null) real = loader.get();
            return real;
        }

        public double score(String message) { return subject().score(message); }
    }

    record InboxFilter(SpamClassifier classifier, Set<String> trusted) {
        boolean isSpam(String sender, String body) {
            if (trusted.contains(sender)) return false; // most mail stops here
            return classifier.score(body) >= 0.7;
        }
    }

    public static void main(String[] args) {
        InboxFilter filter = new InboxFilter(
            new LazyClassifier(() -> new ModelClassifier("models/spam-v3.bin")),
            Set.of("billing@acme.io", "ci@acme.io"));
        System.out.println("service ready");
        System.out.println(filter.isSpam("billing@acme.io", "Your invoice is ready"));         // false, no load
        System.out.println(filter.isSpam("promo@deals.biz", "WINNER: claim free crypto now")); // loads once, true
        System.out.println(filter.isSpam("friend@mail.com", "Lunch tomorrow?"));              // false
    }
}
```

The output shows `service ready` before `loading model`, and the model loads exactly once. A protection proxy has the same shape: replace the `??=` line with a permission check that throws before delegating.

## Trade-offs

**Gains**
- Clients get access control, laziness or remoteness without any code change.
- The rule is enforced in one place, so no call site can skip it.
- Startup is faster and memory is spent only when the subject is actually used.
- The real subject stays focused on its job.

**Costs**
- The first call pays the full setup cost, which can surprise latency budgets. Warm it in the background if that matters.
- Lazy creation in concurrent code needs correct synchronization.
- Errors move: a broken model file now fails on the first message, not at startup. Add a readiness check if you need fail-fast.
- Remote proxies that look local hide latency and partial failure.

## Relations

- [Decorator](https://aipatterns.dev/design-patterns/decorator.md) has the same structure. A decorator adds behavior and is stacked by the client around an object it already has; a proxy controls access and usually owns the subject's creation and lifetime.
- [Adapter](https://aipatterns.dev/design-patterns/adapter.md) gives its wrapped object a different interface. A proxy keeps the interface identical.
- [Facade](https://aipatterns.dev/design-patterns/facade.md) presents a new, simpler interface over several objects. A proxy stands in for one object with the same interface.
- [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) saves memory by sharing many objects. A virtual proxy saves memory by not creating one until needed.

## Agent checklist

- [ ] The subject is measurably expensive, remote, or needs access control; otherwise use it directly.
- [ ] You checked for a language-native option first: `cached_property`, `sync.OnceValue`, JavaScript `Proxy`, dynamic proxies.
- [ ] Clients depend on the interface, and the proxy implements all of it.
- [ ] Lazy creation is thread-safe where the proxy is shared, and creation errors are surfaced, not swallowed.
- [ ] Tests cover the first call (creation), later calls (reuse), and denied calls for a protection proxy.
