BrandGhost
When to Use Proxy Pattern in C#: Decision Guide with Examples

When to Use Proxy Pattern in C#: Decision Guide with Examples

When to Use Proxy Pattern in C#: Decision Guide with Examples

You've built a service that talks to an external API, queries a database, or loads a massive resource into memory. It works. But then you realize the object is too expensive to create eagerly, some callers shouldn't have access, and the same data gets fetched over and over again. These are the exact problems the proxy pattern in C# is designed to solve -- and knowing when to reach for it separates thoughtful architecture from pattern overuse.

This article gives you a structured decision guide for choosing the proxy pattern in C#. We'll walk through the major proxy variants -- virtual, protection, caching, and logging -- with concrete code examples. Just as importantly, we'll cover when the proxy pattern is the wrong choice and when simpler alternatives like Lazy<T> or AOP interceptors serve you better. If you want broader context on structural patterns, the adapter pattern and facade pattern solve related but distinct problems.

What the Proxy Pattern Actually Does

The proxy pattern provides a surrogate or placeholder for another object. The proxy implements the same interface as the real object, so clients interact with it identically. The difference is that the proxy controls access to the real object -- deciding when to create it, who can call it, whether to cache results, or what to log before and after delegation.

This "controlled access" intent is what separates a proxy from a decorator. A decorator adds behavior. A proxy controls access. The structural implementation looks nearly identical, but the intent drives different design decisions.

Signs You Need the Proxy Pattern in C#

Certain patterns in your codebase signal that a proxy is the right tool. Here are the key indicators to watch for.

Expensive Object Creation Is Happening Eagerly

If your application creates heavyweight objects at startup -- objects that load large datasets, open network connections, or allocate significant memory -- and those objects might not even be used during a given execution, you're paying a cost for nothing. A virtual proxy defers that creation until the first method call.

Access Control Lives Inside Business Logic

When authorization checks are scattered throughout your service methods, the business logic gets polluted with security concerns. A protection proxy centralizes access control at the boundary, keeping the real service focused on its core responsibility.

The Same Data Gets Fetched Repeatedly

If your service makes expensive calls -- database queries, API requests, file reads -- and the results don't change frequently, a caching proxy can intercept those calls and return cached results without touching the real service. This is especially valuable when multiple callers request the same data within a short window.

You Need Observability Without Modifying the Service

Logging, auditing, and metrics are important, but embedding them inside a service class mixes operational concerns with business logic. A logging proxy wraps the real service and records method calls, arguments, and results without the real service knowing anything about it.

Scenario 1: Virtual Proxy with Lazy Initialization

The virtual proxy is one of the most common proxy variants. It defers creation of an expensive object until it's actually needed. C# has Lazy<T> built into the framework, and a virtual proxy builds on that concept while maintaining interface compatibility.

Consider a report engine that loads a large dataset from a database:

public interface IReportEngine
{
    ReportResult Generate(string reportId);
}

public record ReportResult(
    string ReportId,
    string Content,
    DateTimeOffset GeneratedAt);

public sealed class HeavyReportEngine : IReportEngine
{
    public HeavyReportEngine()
    {
        // Simulate expensive initialization
        Console.WriteLine(
            "[HeavyReportEngine] Loading large dataset...");
        Thread.Sleep(3000);
    }

    public ReportResult Generate(string reportId)
    {
        return new ReportResult(
            reportId,
            $"Full analysis for {reportId}",
            DateTimeOffset.UtcNow);
    }
}

Without a proxy, the HeavyReportEngine gets created at startup even if no report is ever requested. Here's the virtual proxy:

public sealed class VirtualReportEngineProxy : IReportEngine
{
    private readonly Lazy<IReportEngine> _engine;

    public VirtualReportEngineProxy(
        Func<IReportEngine> factory)
    {
        _engine = new Lazy<IReportEngine>(factory);
    }

    public ReportResult Generate(string reportId)
    {
        return _engine.Value.Generate(reportId);
    }
}

Wire it up in your composition root:

IReportEngine engine = new VirtualReportEngineProxy(
    () => new HeavyReportEngine());

// No initialization happens until this call
var result = engine.Generate("quarterly-sales");

The proxy uses Lazy<T> internally, but the key difference from using Lazy<T> directly is that the proxy implements IReportEngine. Callers don't need to know they're working with a proxy -- they depend on the interface and the proxy handles the lazy initialization transparently.

Scenario 2: Protection Proxy for Access Control

A protection proxy enforces authorization rules before delegating to the real service. This keeps security logic out of the business layer and makes it easy to test independently.

public interface IDocumentService
{
    Document GetDocument(string documentId);
    void DeleteDocument(string documentId);
}

public record Document(
    string Id,
    string Title,
    string Content);

public sealed class DocumentService : IDocumentService
{
    public Document GetDocument(string documentId)
    {
        Console.WriteLine(
            $"Fetching document {documentId} from storage");
        return new Document(
            documentId,
            "Confidential Report",
            "Sensitive content here");
    }

    public void DeleteDocument(string documentId)
    {
        Console.WriteLine(
            $"Deleting document {documentId} from storage");
    }
}

The protection proxy checks permissions before allowing the call through:

public sealed class ProtectionDocumentProxy : IDocumentService
{
    private readonly IDocumentService _inner;
    private readonly IUserContext _userContext;

    public ProtectionDocumentProxy(
        IDocumentService inner,
        IUserContext userContext)
    {
        _inner = inner
            ?? throw new ArgumentNullException(nameof(inner));
        _userContext = userContext
            ?? throw new ArgumentNullException(
                nameof(userContext));
    }

    public Document GetDocument(string documentId)
    {
        RequirePermission("documents:read");
        return _inner.GetDocument(documentId);
    }

    public void DeleteDocument(string documentId)
    {
        RequirePermission("documents:delete");
        _inner.DeleteDocument(documentId);
    }

    private void RequirePermission(string permission)
    {
        if (!_userContext.HasPermission(permission))
        {
            throw new UnauthorizedAccessException(
                $"User '{_userContext.UserId}' lacks " +
                $"permission '{permission}'");
        }
    }
}

public interface IUserContext
{
    string UserId { get; }
    bool HasPermission(string permission);
}

Usage is straightforward -- callers interact with IDocumentService and the proxy intercepts every call:

IDocumentService service = new ProtectionDocumentProxy(
    new DocumentService(),
    currentUserContext);

// Throws UnauthorizedAccessException if user lacks
// "documents:read" permission
var doc = service.GetDocument("doc-42");

This approach pairs well with inversion of control containers where you can register the proxy as the implementation for IDocumentService and swap between protected and unprotected variants based on environment.

Scenario 3: Caching Proxy for Frequently Accessed Data

When a service makes expensive calls and the results are stable over short periods, a caching proxy eliminates redundant work. Unlike a caching decorator (which adds caching as one of many composable behaviors), a caching proxy typically acts as the single access gateway -- it's the controlled entry point for the real service.

public interface IProductCatalog
{
    Product GetProduct(string productId);
    IReadOnlyList<Product> SearchProducts(string query);
}

public record Product(
    string Id,
    string Name,
    decimal Price);

public sealed class CachingProductCatalogProxy
    : IProductCatalog
{
    private readonly IProductCatalog _inner;
    private readonly ConcurrentDictionary<string,
        (Product Data, DateTimeOffset Expiry)> _productCache
            = new();
    private readonly TimeSpan _ttl;

    public CachingProductCatalogProxy(
        IProductCatalog inner,
        TimeSpan ttl)
    {
        _inner = inner
            ?? throw new ArgumentNullException(nameof(inner));
        _ttl = ttl;
    }

    public Product GetProduct(string productId)
    {
        if (_productCache.TryGetValue(
                productId, out var cached)
            && cached.Expiry > DateTimeOffset.UtcNow)
        {
            return cached.Data;
        }

        var product = _inner.GetProduct(productId);
        _productCache[productId] = (
            product,
            DateTimeOffset.UtcNow.Add(_ttl));
        return product;
    }

    public IReadOnlyList<Product> SearchProducts(
        string query)
    {
        // Search results are not cached because
        // query combinations are unbounded
        return _inner.SearchProducts(query);
    }
}

Notice that the proxy makes a deliberate decision about what to cache. GetProduct caches individual lookups with a TTL. SearchProducts passes through directly because caching arbitrary search queries would consume unbounded memory. This selective caching is a design decision that belongs in the proxy, not in the real service.

Scenario 4: Logging Proxy for Auditing

A logging proxy records method invocations without modifying the real service. This is particularly useful for audit trails, debugging, and compliance requirements.

public sealed class AuditingDocumentProxy : IDocumentService
{
    private readonly IDocumentService _inner;
    private readonly ILogger _logger;

    public AuditingDocumentProxy(
        IDocumentService inner,
        ILogger logger)
    {
        _inner = inner
            ?? throw new ArgumentNullException(nameof(inner));
        _logger = logger
            ?? throw new ArgumentNullException(nameof(logger));
    }

    public Document GetDocument(string documentId)
    {
        _logger.LogInformation(
            "Document access: {DocumentId}", documentId);
        return _inner.GetDocument(documentId);
    }

    public void DeleteDocument(string documentId)
    {
        _logger.LogWarning(
            "Document deletion: {DocumentId}", documentId);
        _inner.DeleteDocument(documentId);
    }
}

The proxy logs access and deletion events at appropriate severity levels. The real DocumentService remains clean -- it handles documents and nothing else. This separation makes both the service and the audit logic easier to test independently.

When NOT to Use the Proxy Pattern

Knowing when to use the proxy pattern in C# is only half the equation. Reaching for it in the wrong situation creates unnecessary indirection.

Lazy<T> Already Solves Your Problem

If you just need lazy initialization and every caller already works with a factory or Lazy<T>, wrapping it in a proxy adds a class for no benefit. The proxy earns its place when callers depend on an interface and shouldn't know about the lazy loading. If there's only one caller and it can accept a Lazy<T> directly, skip the proxy.

Simple Delegation Without Access Control

If your "proxy" just forwards every call to the real object without any logic -- no caching, no authorization, no lazy loading -- it's not a proxy. It's unnecessary indirection. A proxy must add value by controlling access in some meaningful way.

AOP or Interceptors Handle It Better

When you need logging, metrics, or retry logic applied uniformly across dozens of services, writing individual proxies for each service doesn't scale. AOP frameworks and DI interceptors (like Castle DynamicProxy or Scrutor decorators) generate these wrappers automatically. If the cross-cutting concern is generic and applies broadly, interceptors are the better tool.

The Interface Is Too Broad

Just like with decorators, a proxy works best with narrow interfaces. If your interface has twenty methods and the proxy only cares about three of them, you'll write seventeen pass-through methods that add noise without value. Consider splitting the interface first.

Decision Criteria Checklist

Walk through these questions when evaluating whether the proxy pattern in C# is the right fit:

  1. Does the real object need controlled access? If no one needs to gate, delay, cache, or observe access to the object, a proxy adds nothing.
  2. Should clients be unaware of the control logic? The proxy pattern works because clients code against the interface. If callers are fine with explicit Lazy<T> or cache lookups, a proxy isn't necessary.
  3. Is there exactly one primary concern? Proxies work best when they handle a single access-control responsibility -- lazy loading, authorization, caching, or logging. If you need to compose multiple concerns, the decorator pattern is better suited.
  4. Is the interface narrow enough? A proxy with many pass-through methods signals an interface that's too broad. Refactor toward smaller interfaces before applying the proxy.
  5. Would an interceptor or AOP approach scale better? If you need the same concern applied to many services, code generation or interception frameworks save you from writing repetitive proxy classes.

If you answer "yes" to questions 1 through 4 and "no" to question 5, the proxy pattern is likely the right choice.

Proxy vs Decorator: When to Pick Which

The proxy and decorator patterns are structurally almost identical -- both implement the same interface as the wrapped object and delegate to it. The difference is intent, and that intent drives real architectural decisions.

Use a proxy when you control access. A virtual proxy decides when the real object is created. A protection proxy decides who can call it. A caching proxy decides whether to call it at all. The proxy stands between the client and the real object as a gatekeeper.

Use a decorator when you add behavior. A logging decorator records what happened. A retry decorator repeats on failure. A validation decorator checks inputs. Decorators don't gate access -- they layer behavior on top of an operation that will still execute.

The composability test is the clearest signal. If you plan to stack multiple wrappers -- logging plus caching plus metrics -- you want decorators. Decorators are designed for composition. If you need a single wrapper that manages one access-control concern, a proxy is more appropriate. You don't typically stack proxies the way you stack decorators.

Consider a document service that needs both protection and audit logging. You could build this with a protection proxy and a logging decorator, or with two decorators. The strategy pattern is the right choice when you need to swap the entire implementation rather than wrap it.

Here's a quick comparison:

Criteria Proxy Decorator
Primary intent Control access to the real object Add behavior around the real object
Typical count One per service Multiple, composed in a chain
Manages lifecycle Often (lazy init, pooling) Rarely
Composability Not designed for stacking Built for stacking
Best for Lazy loading, auth, caching Logging, retry, metrics, validation

Understanding this distinction helps you pick the right pattern and communicate your intent clearly to other developers on your team.

Frequently Asked Questions

What is the difference between a proxy and a wrapper in C#?

A wrapper is a general term for any object that contains another object and delegates to it. A proxy is a specific type of wrapper that implements the same interface as the wrapped object and controls access to it. All proxies are wrappers, but not all wrappers are proxies. A facade, for example, wraps multiple objects behind a simplified interface -- which is a different intent entirely.

Can I combine the proxy pattern with dependency injection in C#?

Absolutely. Register the real service as a concrete type and the proxy as the interface implementation. The DI container resolves the proxy, which internally receives the real service. This works with any standard DI container. The key is that callers depend on the interface and the container decides whether to provide the real service or the proxy -- which is exactly how inversion of control is supposed to work.

When should I use Lazy<T> instead of a virtual proxy?

Use Lazy<T> directly when the caller can accept a Lazy<T> parameter and there's no need to hide the lazy loading behind an interface. Use a virtual proxy when callers depend on an interface and shouldn't know that lazy initialization is happening. The proxy wraps Lazy<T> internally while exposing the same interface as the real object, keeping the lazy loading transparent.

How does the proxy pattern relate to the adapter pattern?

Both patterns wrap another object, but for different reasons. The adapter pattern converts one interface to another so that incompatible types can work together. The proxy pattern keeps the same interface and controls access to the real object. An adapter changes shape; a proxy controls the gate.

Is the proxy pattern useful for unit testing?

Yes, but not in the way you might expect. The proxy pattern itself doesn't replace mocking frameworks. However, designing your services with interface-based access makes them inherently testable. You can test a protection proxy by passing a mock service and verifying that unauthorized calls throw exceptions. You can test a caching proxy by verifying that the inner service is only called once for repeated requests. The pattern encourages the kind of interface-driven design that makes testing straightforward.

Should I use the proxy pattern or the state pattern for conditional behavior?

These patterns solve fundamentally different problems. The proxy pattern controls access to an object -- deciding when, whether, or how to delegate. The state pattern changes an object's behavior based on its internal state. If your object behaves differently depending on whether it's "active," "suspended," or "closed," that's state. If you need to guard access to an object based on external factors like permissions or initialization status, that's a proxy.

Does the proxy pattern add noticeable performance overhead?

The proxy adds one level of indirection -- a single method call through the proxy before reaching the real object. For the vast majority of applications, this overhead is negligible. In fact, caching and virtual proxies typically improve performance by avoiding expensive operations. The only scenario where proxy overhead matters is in extremely tight loops where you're calling through the proxy millions of times per second, and even then, the JIT compiler often inlines simple delegation.

Wrapping Up the Proxy Pattern Decision Guide

Deciding when to use the proxy pattern in C# comes down to one core question: do you need to control access to an object while keeping that control transparent to callers? If the answer is yes -- whether for lazy initialization, authorization, caching, or auditing -- the proxy pattern gives you a clean, interface-compatible way to do it.

The decision framework is direct. Check whether the real object needs controlled access, whether clients should remain unaware of that control, whether the interface is narrow enough to proxy cleanly, and whether a single concern justifies a dedicated proxy class. If you need to compose multiple cross-cutting behaviors, lean toward the decorator pattern instead. If you need the same concern applied across dozens of services, consider AOP interceptors over hand-written proxies.

Start with the simplest solution that meets your requirements. If Lazy<T> alone handles your deferred initialization and callers can work with it directly, use that. If you need transparent access control behind an interface, build a proxy. The pattern earns its place when it simplifies your architecture -- not when it adds layers of indirection for their own sake.

Proxy Design Pattern in C#: Complete Guide with Examples

Master the proxy design pattern in C# with practical examples showing virtual proxies, protection proxies, caching proxies, and lazy loading implementations.

Decorator vs Proxy Pattern in C#: Key Differences Explained

Compare decorator vs proxy pattern in C# with side-by-side code examples, key differences explained, and guidance on when to use each pattern.

How to Implement Proxy Pattern in C#: Step-by-Step Guide

Learn how to implement the proxy pattern in C# with step-by-step examples covering virtual proxies, protection proxies, and DI registration.

An error has occurred. This application may no longer respond until reloaded. Reload