Standing in for the real thing
A celebrity’s assistant answers the phone with the same “Hello, this is Ana’s line” every time. Simple questions (“Is she in town next week?”) the assistant answers alone; requests from strangers never get through; only the important calls reach Ana herself. The caller talks to the same number either way.
A proxy works the same way. It has exactly the same methods as the real object, so code that uses it doesn’t know (or care) which one it has. Inside each method, the proxy decides: answer by itself, refuse, or pass the call on to the real object.
A Proxy provides a surrogate with the subject’s interface to control access to it. The common variants: virtual (defers creation of an expensive subject), protection (authorization checks), caching (memoizes results), remote (stub for an object in another process: gRPC stubs, Retrofit interfaces, RMI), and smart reference (reference counting, logging, locking). Structurally it’s identical to a Decorator; the intent differs: a decorator adds responsibilities, a proxy manages access to and the lifecycle of the subject, often creating it itself.
protocol Document {
var title: String { get }
func content() throws -> String
}
final class FileDocument: Document { // the real, expensive object
let title: String
private let text: String
init(url: URL) throws {
text = try String(contentsOf: url, encoding: .utf8) // reads the whole file
title = url.deletingPathExtension().lastPathComponent
}
func content() throws -> String { text }
}
final class LazyDocument: Document { // the proxy
let title: String
private let url: URL
private var real: FileDocument?
init(url: URL) {
self.url = url
title = url.deletingPathExtension().lastPathComponent // cheap: no file access
}
func content() throws -> String {
if real == nil { real = try FileDocument(url: url) } // created on first use
return try real!.content()
}
}interface Document {
val title: String
fun content(): String
}
class FileDocument(file: File) : Document { // the real, expensive object
private val text = file.readText() // reads the whole file
override val title = file.nameWithoutExtension
override fun content() = text
}
class LazyDocument(private val file: File) : Document { // the proxy
override val title = file.nameWithoutExtension // cheap: no file access
private val real by lazy { FileDocument(file) } // created on first use
override fun content() = real.content()
}public interface Document {
String title();
String content();
}
public final class FileDocument implements Document { // the real, expensive object
private final String title;
private final String text;
public FileDocument(Path path) throws IOException {
this.text = Files.readString(path); // reads the whole file
this.title = path.getFileName().toString().replaceFirst("\\.[^.]+$", "");
}
public String title() { return title; }
public String content() { return text; }
}
public final class LazyDocument implements Document { // the proxy
private final Path path;
private FileDocument real;
public LazyDocument(Path path) { this.path = path; }
public String title() { return path.getFileName().toString().replaceFirst("\\.[^.]+$", ""); }
public synchronized String content() {
if (real == null) {
try { real = new FileDocument(path); } // created on first use
catch (IOException e) { throw new UncheckedIOException(e); }
}
return real.content();
}
}interface Document {
readonly title: string;
content(): Promise<string>;
}
class FileDocument implements Document { // the real, expensive object
private constructor(readonly title: string, private readonly text: string) {}
static async load(path: string): Promise<FileDocument> {
const text = await fs.readFile(path, "utf8"); // reads the whole file
return new FileDocument(basename(path, extname(path)), text);
}
async content(): Promise<string> { return this.text; }
}
class LazyDocument implements Document { // the proxy
readonly title: string;
private real?: Promise<FileDocument>;
constructor(private readonly path: string) {
this.title = basename(path, extname(path)); // cheap: no file access
}
async content(): Promise<string> {
this.real ??= FileDocument.load(this.path); // created on first use
return (await this.real).content();
}
}- Same interface: the list screen works with
Document, and getsLazyDocuments. It shows 200 titles instantly, because titles come from the file name, not the file. - On first use of
content(), the proxy creates the realFileDocumentand passes the call on. Later calls reuse it. - Nothing else changed: neither the screen nor
FileDocumentknows the proxy exists.
Proxies you already use
- ORM lazy associations:
order.customerin Hibernate or JPA is a generated subclass that loads the row on first access. - Remote stubs: a Retrofit interface, a gRPC client stub or a Ktor/OpenAPI-generated client makes a network call look like a method call.
- Spring AOP:
@Transactional,@Cacheableand@Asyncbeans are proxies that run logic around your method. - JavaScript
Proxy, Swift@dynamicMemberLookupwrappers and Kotlin’sbydelegation make generic proxies possible without writing each method.
Predict the output
The list creates 200 LazyDocument objects and shows their titles. The user opens one document and reads it twice. How many files are read from disk?
Titles come from the file names, so showing the list reads no files. Opening a document calls content(), which creates the real FileDocument (one file read); the second read reuses it. One file in total, instead of 200 at start-up. That's the point of a virtual proxy: pay only for what's used.
- Same interface as the subject; controls access and lifecycle. Variants: virtual (lazy creation), protection (authz), caching, remote (stubs), smart reference.
- Structurally a Decorator; intent differs (access and lifecycle vs added responsibilities).
- In the wild: ORM lazy associations, Retrofit/gRPC stubs, Spring AOP proxies, JS
Proxy, Kotlinbydelegation.
Check this part
What does a proxy have in common with the real object?
What does a virtual (lazy) proxy do?
Proxy and Decorator have the same structure. What differs?