BrandGhost
Proxy vs Decorator Pattern in C#: Key Differences Explained

Proxy vs Decorator Pattern in C#: Key Differences Explained

Proxy vs Decorator Pattern in C#: Key Differences Explained

Few design pattern comparisons cause as much confusion as this one. Both the proxy and the decorator wrap an object behind the same interface. Both forward method calls. Both look nearly identical in a class diagram. So when a developer asks "which one do I need?" the answer rarely comes down to structure -- it comes down to why you're wrapping in the first place. Understanding the proxy vs decorator pattern in C# distinction is essential for writing code that communicates intent clearly, because choosing the wrong wrapper means your design says one thing while your code does another.

The proxy pattern controls access to an object. The decorator pattern adds behavior to an object. That single sentence is the entire difference, but unpacking it requires real code and real scenarios. In this article, we'll compare both patterns side by side against the same interface, explore the subtle but important differences in lifecycle ownership, transparency, and DI registration, and walk through a practical scenario where both patterns solve the same problem in fundamentally different ways. For a standalone deep dive into decorators, see Decorator Design Pattern in C#: Complete Guide.

Quick Refresher: The Proxy Pattern

The proxy pattern places a stand-in object between the client and the real service. The proxy implements the same interface as the real object, but its purpose is to manage how and whether the client reaches that real object. A proxy might delay instantiation of an expensive resource until the first call comes in. It might check credentials before letting a request pass through. It might cache a response and skip the inner call entirely next time.

The defining trait of a proxy is gatekeeping. The real object may or may not get called, and the proxy makes that decision. Common proxy types include:

  • Virtual proxy -- defers creation of the real object until the first use.
  • Protection proxy -- enforces authorization before forwarding.
  • Caching proxy -- returns stored results to avoid repeated work.
  • Remote proxy -- represents an object across a network boundary.

Quick Refresher: The Decorator Pattern

The decorator pattern wraps an existing object to layer additional behavior on top of its existing functionality. Like the proxy, the decorator implements the same interface and holds a reference to the inner object. But unlike the proxy, the decorator always calls the inner object. It never decides to skip the call, block the call, or substitute its own result in place of the real one.

Decorators excel at cross-cutting concerns -- logging, timing, validation, retry logic, metrics -- where the behavior is independent of the business logic and can be composed in any order. You can stack multiple decorators to build a pipeline of behavior, and each one operates without knowledge of the others.

Structural Similarity: Nearly Identical Class Diagrams

Here's why the proxy vs decorator pattern confusion exists. Draw a UML class diagram for each pattern and you'll end up with nearly the same picture:

  • An interface (IService)
  • A concrete implementation (RealService)
  • A wrapper class that implements IService, holds a reference to IService, and delegates calls

Both patterns share these structural traits:

  • Same interface. The wrapper, the real object, and any other wrappers all implement the same interface. Client code never knows it's talking to a wrapper.
  • Composition over inheritance. Both use an internal reference to the wrapped object rather than subclassing it.
  • Transparent substitution. You can pass either wrapper anywhere the real object is expected.

This structural overlap is what makes pattern identification tricky. You can't tell which pattern you're looking at by reading the class signature alone. You have to look at what the wrapper does with its reference to the inner object. Other structural patterns share this wrapping mechanism too -- the Adapter Design Pattern in C# converts interfaces, while the Bridge Design Pattern in C# separates abstraction from implementation -- but they each solve a distinctly different problem.

Intent Difference: Access Control vs Behavior Enhancement

The structural similarity makes it tempting to treat these patterns as interchangeable. They are not. The fundamental difference is intent:

  • Proxy controls access to the object. It decides whether, when, and under what conditions the real object gets involved. The proxy is a gatekeeper.
  • Decorator adds behavior to the object. It enhances what happens before, during, or after the real object's operation. The decorator is an enhancer.

A proxy might reject a call outright, delay instantiation, serve a cached result, or translate a local call into a remote one. A decorator might log, measure, validate, or retry -- but it always lets the real call proceed. This intent distinction is what separates two patterns that otherwise look identical in code.

Lifecycle Ownership: Who Creates the Real Object?

One of the more practical differences between proxy and decorator shows up in how each manages the inner object's lifecycle.

A decorator receives the inner object through its constructor. It doesn't know or care how the inner object was created, when it was created, or how long it lives. The decorator's only relationship with the inner object is "I call it and add something around that call."

A proxy often creates or controls the real object. A virtual proxy accepts a factory function and defers creation until the first method call. A protection proxy might hold the sole reference to the real service. A remote proxy manages the connection to a distant resource. The proxy owns the lifecycle.

This matters for testability and composition. Because decorators receive their dependencies, they're trivially mockable -- hand in a fake and assert the decorator's behavior. Proxies that manage lifecycle internally require more setup for testing, since the creation logic itself may need to be verified.

Transparency: Full Delegation vs Restricted Access

A decorator is fully transparent to the inner object. It always delegates. It never withholds or substitutes. The inner object will always execute when the decorator is called.

A proxy is selectively transparent. It may delegate, but it reserves the right not to. A protection proxy throws if authorization fails. A caching proxy short-circuits the call when cached data is available. A virtual proxy delays the call until the real object exists. The proxy's value comes precisely from the fact that it does not always delegate.

This distinction drives an important architectural decision: if you need guaranteed execution of the inner logic with optional enhancements, use a decorator. If you need conditional execution with access control semantics, use a proxy.

Same Problem, Both Ways: Logging

The best way to see the proxy vs decorator pattern in C# difference in action is to solve the same problem with both patterns. Let's take logging -- one of the most common cross-cutting concerns -- and implement it as both a decorator and a proxy using the same interface.

The Shared Interface and Implementation

public interface IPaymentProcessor
{
    PaymentResult ProcessPayment(
        string customerId,
        decimal amount);
}

public sealed class PaymentResult
{
    public bool Success { get; }

    public string TransactionId { get; }

    public PaymentResult(bool success, string transactionId)
    {
        Success = success;
        TransactionId = transactionId;
    }
}

public sealed class StripePaymentProcessor : IPaymentProcessor
{
    public PaymentResult ProcessPayment(
        string customerId,
        decimal amount)
    {
        Console.WriteLine(
            $"[Stripe] Charging {amount:C} " +
            $"for customer {customerId}");
        return new PaymentResult(
            true,
            Guid.NewGuid().ToString("N"));
    }
}

Decorator Approach: Logging Method Calls

The decorator logs every method call -- entry, exit, duration, and result. It always invokes the inner processor.

using System.Diagnostics;

public sealed class LoggingPaymentDecorator
    : IPaymentProcessor
{
    private readonly IPaymentProcessor _inner;

    public LoggingPaymentDecorator(
        IPaymentProcessor inner)
    {
        _inner = inner;
    }

    public PaymentResult ProcessPayment(
        string customerId,
        decimal amount)
    {
        Console.WriteLine(
            $"[LOG] Processing payment: " +
            $"customer={customerId}, amount={amount:C}");

        var stopwatch = Stopwatch.StartNew();
        var result = _inner.ProcessPayment(
            customerId, amount);
        stopwatch.Stop();

        Console.WriteLine(
            $"[LOG] Payment completed: " +
            $"success={result.Success}, " +
            $"txn={result.TransactionId}, " +
            $"elapsed={stopwatch.ElapsedMilliseconds}ms");

        return result;
    }
}

The decorator never questions whether the call should proceed. It adds observability and delegates unconditionally.

Proxy Approach: Logging Access Control Decisions

The proxy also logs, but it logs something fundamentally different -- access control decisions. It logs why a call was allowed or denied, not what the call did.

public sealed class AuditPaymentProxy : IPaymentProcessor
{
    private readonly IPaymentProcessor _inner;
    private readonly IAuthorizationService _authService;

    public AuditPaymentProxy(
        IPaymentProcessor inner,
        IAuthorizationService authService)
    {
        _inner = inner;
        _authService = authService;
    }

    public PaymentResult ProcessPayment(
        string customerId,
        decimal amount)
    {
        bool authorized = _authService
            .CanProcessPayment(customerId, amount);

        if (!authorized)
        {
            Console.WriteLine(
                $"[AUDIT] DENIED: payment of {amount:C} " +
                $"for customer {customerId} -- " +
                $"authorization failed");
            throw new UnauthorizedAccessException(
                $"Payment denied for customer {customerId}");
        }

        Console.WriteLine(
            $"[AUDIT] ALLOWED: payment of {amount:C} " +
            $"for customer {customerId}");

        return _inner.ProcessPayment(
            customerId, amount);
    }
}

public interface IAuthorizationService
{
    bool CanProcessPayment(
        string customerId,
        decimal amount);
}

Both classes wrap the same interface. Both produce log output. But the decorator logs what happened during the call, while the proxy logs whether the call was permitted at all. The decorator always delegates. The proxy may throw instead.

DI Integration: Similar Registration, Different Purposes

In a typical ASP.NET Core application, both proxies and decorators are registered through IServiceCollection using factory delegates. The registration syntax looks similar, but the intent behind each registration is different.

using Microsoft.Extensions.DependencyInjection;

public static class PaymentServiceRegistration
{
    public static IServiceCollection AddPaymentProcessing(
        this IServiceCollection services)
    {
        services.AddScoped<StripePaymentProcessor>();
        services.AddScoped<IAuthorizationService,
            DefaultAuthorizationService>();

        services.AddScoped<IPaymentProcessor>(sp =>
        {
            // Core implementation
            IPaymentProcessor processor =
                sp.GetRequiredService<
                    StripePaymentProcessor>();

            // Proxy layer: access control
            processor = new AuditPaymentProxy(
                processor,
                sp.GetRequiredService<
                    IAuthorizationService>());

            // Decorator layer: observability
            processor = new LoggingPaymentDecorator(
                processor);

            return processor;
        });

        return services;
    }
}

The proxy registration is about enforcing business rules -- authorization, rate limiting, resource gating. The decorator registration is about adding operational concerns -- logging, metrics, tracing. They compose naturally because each layer has a clear, independent purpose. This separation of concern is reminiscent of how the Strategy Design Pattern in C# isolates interchangeable algorithms behind a common interface.

Can They Work Together?

Yes -- and they often should. Because proxies and decorators share the same interface, they compose seamlessly. You can wrap a proxy with a decorator, wrap a decorated service with a proxy, or stack both in whatever order your requirements dictate.

Decorator Wrapping a Proxy

This is the most common composition. The proxy sits closest to the real object, controlling access, while the decorator sits on the outside, adding behavior visible to callers:

// Proxy controls access, decorator adds logging
IPaymentProcessor processor =
    new StripePaymentProcessor();
processor = new AuditPaymentProxy(
    processor,
    authService);
processor = new LoggingPaymentDecorator(processor);

When a call arrives, the logging decorator records entry, the audit proxy checks authorization, and if approved, the real processor handles the payment. The decorator sees the full lifecycle including any authorization exceptions, which gives you comprehensive audit trails.

Proxy Wrapping a Decorated Service

Less common but valid. If you have a service already decorated with retry or caching logic, a proxy can gate access to the entire decorated pipeline:

// Decorators add behavior, proxy gates the whole thing
IPaymentProcessor processor =
    new StripePaymentProcessor();
processor = new RetryPaymentDecorator(processor);
processor = new LoggingPaymentDecorator(processor);
processor = new AuditPaymentProxy(
    processor,
    authService);

Here, the proxy denies unauthorized callers before any decorator runs. Retries and logging only happen for callers who pass authorization. The order changes the behavior, so choose the composition that matches your requirements.

The ability to freely compose these patterns is one of the strengths of interface-based design. The Facade Design Pattern in C# takes a different approach by unifying multiple interfaces behind one simplified API, but the composability you get with proxy and decorator is a direct result of both patterns honoring the same contract.

Decision Criteria Table

Use this reference to choose between proxy and decorator when the structural similarity makes the decision unclear:

Criteria Choose Proxy Choose Decorator
You need to... Control whether the call happens Add behavior around the call
Inner object is called... Conditionally Always
Inner object lifecycle Proxy creates or manages it Receives it from outside
Authorization / permissions Yes -- gatekeeper No -- not its concern
Lazy initialization Yes -- deferred creation No -- needs the object upfront
Cross-cutting concerns Only access-related ones Logging, metrics, retry, validation
Stacking multiple wrappers Uncommon -- usually one proxy Common -- multiple decorators compose
Caching semantics Skip inner call on cache hit Always call inner, cache result for later
Testing approach Mock the factory or inner service Mock the inner service directly
DI purpose Enforce business rules Add operational behavior

When both access control and behavior enhancement are needed, use both patterns together. Neither pattern is a substitute for the other.

Frequently Asked Questions

What is the main difference between proxy and decorator in C#?

The main difference is intent. A proxy controls access to an object -- it decides whether, when, and how the real object participates. A decorator adds behavior to an object -- it enhances the interaction but always delegates to the inner object. Despite having nearly identical class structures, the proxy vs decorator pattern in C# distinction is entirely about what problem each wrapper solves.

Can a caching layer be either a proxy or a decorator?

Yes, and which one depends on how caching is implemented. A caching decorator always calls the inner object and stores the result for future reference. A caching proxy checks its cache first and skips the inner call entirely when valid data exists. The proxy gates access; the decorator enhances behavior. If your cache prevents the real call from happening, it's a proxy. If your cache stores results after the real call executes, it's a decorator.

How do proxy and decorator differ from the adapter pattern?

The Adapter Design Pattern in C# converts one interface into another that the client expects. Both proxy and decorator preserve the original interface -- the wrapper implements the same contract as the wrapped object. The adapter exists because of interface incompatibility. The proxy exists to control access. The decorator exists to add behavior. All three are structural patterns, but they solve fundamentally different problems.

Should I use a proxy or decorator for retry logic?

Retry logic is a decorator concern. The wrapper always intends for the inner call to execute -- it just tries again if the first attempt fails. A proxy would make sense if the retry involved checking conditions before re-attempting (such as verifying a circuit breaker state), but pure retry-on-failure is a behavior enhancement, which is decorator territory.

Does the order matter when combining proxy and decorator?

Absolutely. If the proxy is inside and the decorator outside, the decorator sees everything -- including authorization failures that the proxy throws. If the proxy is outside and the decorator inside, unauthorized callers are blocked before any decorator logic runs. Neither ordering is inherently better; it depends on whether you want decorators like logging to capture denied requests or only successful ones.

How do I test a proxy vs a decorator?

Decorators are straightforward to test because they receive the inner object through the constructor. Pass in a mock, invoke the decorator, and assert that additional behavior occurred alongside the delegated call. Proxies can require more setup because they may create or manage the inner object internally. For proxies that accept factories, pass a factory that returns a mock. For proxies with internal lifecycle management, focus on testing the access-control logic by verifying which calls get through.

When should I avoid using either pattern?

Skip both patterns when a simpler approach works. If your framework already provides middleware, filters, or interceptors for cross-cutting concerns, you may not need decorators. If your authorization runs at the API gateway or middleware level, you may not need protection proxies. Patterns are tools for specific problems -- not goals in themselves. Overusing wrappers when middleware or the Composite Design Pattern in C# would serve better leads to unnecessary indirection.

Wrapping Up Proxy vs Decorator in C#

The proxy vs decorator pattern in C# comparison boils down to a single question: are you controlling access to the object, or adding behavior around it? Despite nearly identical class diagrams, these patterns exist for completely different reasons. A proxy decides if and when the real object participates. A decorator enhances what happens when it does.

When you need authorization checks, lazy initialization, or conditional execution -- reach for a proxy. When you need logging, metrics, validation, or retry logic -- reach for a decorator. And when you need both, compose them together. The shared interface contract means proxies and decorators stack cleanly, each layer doing exactly one job without knowledge of the others. Stop treating structural similarity as functional equivalence, and these two patterns become some of the most powerful tools in your C# design pattern toolkit.

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

Discover when to use proxy pattern in C# with decision criteria for virtual, protection, and caching proxies plus guidance on simpler alternatives.

Composite vs Decorator Pattern in C#: Key Differences Explained

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

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.

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