Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 27

    Proxy Pattern

    Proxy stands in for a real object and intercepts every call — same interface, different responsibilities.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Proxy stands in for a real object and intercepts every call — same interface, different responsibilities. Four canonical variants: Remote (RPC stub), Virtual (lazy creation), Protection (access control), Smart Reference (logging, ref-counting). Modern equivalents: JPA lazy proxies, Spring @Transactional CGLIB proxies, ES6 Proxy, gRPC client stubs.

    The story

    A B2B API had 3× latency on customer-detail endpoints. Profiling showed each request loading entire customer aggregate eagerly. ORM lazy proxies — created on first access — dropped p95 by 70%. Domain code never knew.

    The business problem

    Eager loading and uncontrolled access waste resources and expose data:

    • Over-fetching: loading relations never used on this code path.
    • Expensive creation: building heavy objects when caller may not need them.
    • Missing gates: no authorization or rate-limit at object boundary.
    • Cross-cutting duplication: tx/logging/caching reimplemented per call site.

    The problem teams faced

    Proxy fits when:

    • Creating real object is expensive and not always needed (Virtual).
    • Real object lives remotely (Remote / RPC stub).
    • Access must be authorized or rate-limited (Protection).
    • Lazy loading, caching, or transactions without modifying real subject (Smart).

    Understanding the topic

    Intent: Provide a surrogate controlling access to another object.

    • Subject — interface shared by RealSubject and Proxy.
    • RealSubject — actual object Proxy stands in for.
    • Proxy — implements Subject, intercepts, delegates when appropriate.
    • Transparent — client treats Proxy as RealSubject.

    Internal architecture

    Virtual proxy for lazy customer aggregate:

    ts
    interface Customer {
    getId(): string
    getOrders(): Promise<Order[]>
    }
    class CustomerProxy implements Customer {
    private real?: Customer
    constructor(
    private id: string,
    private loader: (id: string) => Promise<Customer>,
    ) {}
    getId() { return this.id }
    async getOrders() {
    if (!this.real) this.real = await this.loader(this.id)
    return this.real.getOrders()
    }
    }

    Visual explanation

    Three diagrams cover proxy variants, lazy virtual proxy, and vs Decorator:

    Four proxy variants
    Remote
    RPC stub
    Virtual
    Lazy init
    Protection
    Auth gate
    Smart ref
    Cache · tx · log
    Same structure — different interception responsibility.
    Virtual proxy (lazy load)
    Client calls getOrders()
    Proxy intercepts
    RealSubject null?
    Load on first use
    Delegate
    Forward to real
    Return result
    Transparent
    Pay construction cost only when relation is actually accessed.
    Proxy vs Decorator
    Control access?
    Proxy
    Add behaviour?
    Decorator
    Same interface
    Both
    Lazy · remote · auth
    Proxy specialties
    Proxy manages relationship to subject; Decorator enhances behaviour.

    Informative example

    Before / after — eager load to lazy proxy:

    ts
    // ❌ Eager — loads orders, invoices, addresses every request
    async function getCustomer(id: string) {
    const row = await db.customers.find(id)
    const orders = await db.orders.where({ customerId: id })
    const invoices = await db.invoices.where({ customerId: id })
    return { ...row, orders, invoices }
    }
    // ✅ Lazy proxy — load relations on first access
    class CustomerProxy implements Customer {
    private orders?: Order[]
    async getOrders() {
    if (!this.orders) this.orders = await db.orders.where({ customerId: this.id })
    return this.orders
    }
    }
    // Protection proxy
    class AuthorizedCustomerProxy implements Customer {
    constructor(private inner: Customer, private auth: AuthContext) {}
    async getOrders() {
    if (!this.auth.can("read:orders")) throw new ForbiddenError()
    return this.inner.getOrders()
    }
    }

    Execution workflow

    1Proxy interception lifecycle
    1 / 4

    Client calls Subject

    Receives Proxy via DI or factory.

    Unaware it's not RealSubject.

    Real-world use

    Hibernate/JPA lazy relations, Spring AOP @Transactional, gRPC/Thrift/OpenAPI stubs, Java Proxy.newProxyInstance, Vue 3 reactive Proxy, Nginx reverse proxy, service mesh sidecars.

    Production case study

    Customer detail lazy proxy:

    • Scenario: B2B API loaded full customer aggregate every request.
    • Symptom: 3× latency vs endpoints using subset of data.
    • Decision: ORM lazy proxies on relations; explicit fetch joins for export path.
    • Outcome: p95 down 70%; export path still uses batched fetch — proxies are defaults, not laws.

    Trade-offs

    • Pro: defer expensive work; uniform access control; transparent to clients.
    • Pro: remote/local swap without client changes.
    • Con: N+1 queries if lazy proxies misused (serialization, loops).
    • Con: debugging through proxy layers adds indirection.

    Decision framework

    • Use Virtual proxy when creation cost high and access selective.
    • Use Protection proxy at sensitive domain boundaries.
    • Use Remote proxy for RPC/client stubs — standard pattern.
    • Avoid lazy proxies on code paths that always traverse full graph.

    Best practices

    • Know your access patterns — lazy default, eager for known full-graph paths.
    • Protection proxy at domain boundary — not scattered if-checks.
    • Log proxy creation in dev to catch N+1 early.
    • Explicit fetch/join for batch operations that need full graph.

    Anti-patterns to avoid

    • Lazy proxy + JSON serialization = N+1 disaster.
    • Protection checks duplicated instead of centralized proxy.
    • Proxy that changes interface — that's Adapter.

    Common mistakes

    • Hibernate export serializing lazy collections — 14-minute N+1 (classic).
    • Proxy holding per-request state — wrong scope.
    • Infinite recursion in smart reference counting.

    Debugging tips

    • Enable SQL logging — count queries per request when proxies involved.
    • Compare query count lazy vs explicit fetch for same endpoint.

    Optimization strategies

    • Batch-fetch known relation sets in dedicated read paths.
    • Cache Protection proxy results with TTL for read-heavy auth checks.

    Common misconceptions

    • Proxy ≠ Decorator. Proxy controls access to subject; Decorator adds behaviour.
    • Nginx reverse proxy is Proxy at network scale.
    • Sidecar is Proxy — transparent interception of service traffic.

    Advanced interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    4 questions
    1IntermediateQuestionFour proxy types?+

    Answer

    Remote (RPC stub), Virtual (lazy init), Protection (access control), Smart Reference (cache, tx, ref-count).

    Follow-up

    Spring @Transactional which?
    2AdvancedQuestionProxy vs Decorator?+

    Answer

    Same interface. Proxy controls access/lazy/remote. Decorator adds behaviour (log, retry). Intent differs.

    Follow-up

    Can one class be both?
    3AdvancedQuestionLazy proxy N+1 trap?+

    Answer

    JSON serialization or loop accessing lazy relation triggers one query per item. Fix with fetch join or DTO projection for that path.

    Follow-up

    Hibernate export case?
    4IntermediateQuestionES6 Proxy use case?+

    Answer

    Reactive frameworks intercept property access for change detection — Smart Reference at language level.

    Follow-up

    Vue 3 reactivity?

    Summary

    You can pick proxy variant, implement lazy loading safely, and avoid N+1 traps. Tell the customer aggregate p95 story — if you can do that, you own Proxy.

    Ready to mark this lesson complete?Track your journey across the entire course.