Module 3 · 3. Structural Patterns · Lesson 7 of 12
Decorator and Proxy
Learning outcome
You will be able to: (1) explain the difference between Decorator (extend behavior at runtime through composition) and Proxy (control or mediate access while remaining transparent), (2) decide which pattern fits production needs given forces and trade-offs, and (3) implement both patterns in C# so they can be substituted for the same abstraction in tests and runtime.
Intuition
At a glance:
- Decorator: "I want to add responsibilities to an object — logging, caching, validation, transformations — and combine them at runtime." Decorators wrap an object and add behavior before/after delegating to the wrapped object. Multiple decorators can be stacked.
- Proxy: "I want to mediate access to an object — check permissions, lazily create, control lifetime, or add network marshalling — without the caller changing how it interacts with the object." Proxies present the same interface but add access-control or virtualization semantics.
Concrete mental model: think of Decorator as an onion of behaviors layered around a core object; Proxy is a gatekeeper in front of the object that sometimes refuses, forwards, or substitutes the inner object.
Deep dive
Key similarities
- Both implement the same abstraction as the real object (same interface or base type).
- Both hold a reference to the wrapped/delegated object and forward calls.
Key differences (forces & intent)
- Intent: Decorator's primary force is composition of behavior; Proxy's primary force is control of access or managing a resource.
- Visibility: Decorator typically modifies how an operation behaves (adds side effects, transforms data). Proxy typically keeps behavior identical unless access rules prevent execution or require virtualization.
- Lifetime/resource management: Proxies often lazy-initialize or manage remote resources. Decorators do not typically manage resource creation for the wrapped object.
- Ordering: Multiple decorators' order affects resulting behavior. Proxy is usually a single mediator. You can combine proxy + decorators though (proxy inside decorator or vice versa).
When to prefer Decorator vs Proxy
- Prefer Decorator when you need flexible runtime composition: logging, retries, transformation, metrics. It helps keep single-responsibility for behaviors and avoids subclass explosion.
- Prefer Proxy when you need to add a transparent access-control, lazy loading, caching facade for remote resources, or to represent remote objects locally.
Simple pseudo-structure (both use the same interface):
Trade-offs and costs
- Decorator complexity: many small decorator classes can make stack traces and debugging harder. It is easy to overuse and scatter cross-cutting behavior.
- Proxy pitfalls: mixing authorization logic into a proxy can obscure real enforcement points; ensure you don't rely solely on client-side proxies for security in distributed systems.
Failure modes
- Incorrect ordering: Placing a transformation decorator after a logging decorator may log the untransformed payload when you intended to log the transformed value.
- Breaking transparency: A proxy or decorator should preserve the abstraction contract; throwing unexpected exceptions or changing semantics silently is a misuse.
- Over-decoration: Creating too many one-off decorators leads to hard-to-understand call chains and brittle tests.
- Using Proxy as security-through-obscurity: A proxy in the same process is not a substitute for server-side authorization in distributed deployments.
Interview drill
Short answer prompts:
- How does Decorator support the Open/Closed Principle? (Add behavior by composition without modifying existing classes.)
- When would you prefer a Proxy over a Decorator? (When you need control of access, lazy initialization, remote stubs, or managing resources.)
- What is a misuse signal for Decorator? (When behavior should be centralized; decorators implementing core domain logic instead of orthogonal concerns.)
Micro-tests you can ask an interviewer or be asked to implement in an interview:
- Compose three decorators (compression, logging, metrics) around a sender and show deterministic ordering of their outputs.
- Implement a proxy that denies access for some users and forwards for allowed users while staying interchangeable with the real implementation.
Example interview question (open): Given a legacy service that needs selective logging and per-request authorization, outline a design using Decorator and Proxy. Explain ordering and testing approach.
Revision checklist
- Can the wrapper be transparently substituted for the concrete object? (Yes => both patterns.)
- Is the goal behavior composition or access/lifecycle control? (Composition => Decorator; Access/lifecycle => Proxy.)
- Are there cross-cutting concerns better handled by middleware or AOP? Consider alternatives.
- Are tests asserting behavior of single decorators (unit) and composed chain (integration)?
Production code
Guidelines and best practices:
- Keep decorators focused and small: one responsibility per decorator (logging, metrics, compression).
- Avoid changing the fundamental contract: decorators/proxies should not change expected return types or exception semantics unless documented.
- Prefer composition via dependency injection rather than manual new chains where possible. Use factories to assemble decoration chains consistently.
- For Proxy-based security, enforce security server-side too; proxies are not a replacement for authority checks across boundaries.
Misuse warning: Don't use a Decorator to implement core business rules that should be in the domain model; don't rely only on client-side proxies for access control.
Code walkthrough
The runnable C# example (below in the repository) demonstrates both patterns and why each is preferable to a simpler alternative.
- Why Decorator vs simpler inheritance: Decorators avoid class explosion when combining orthogonal concerns (e.g., Logging + Compression + Metrics) and allow dynamic composition at runtime.
- Why Proxy vs direct call: Proxy centralizes access checks and virtualization (e.g., lazy creation) without changing the caller's type.
The example is deterministic: it compresses by a deterministic vowel-stripping function and prints ordered messages to show how decorators transform the payload, and how proxy allows or denies access. It also includes a modern C# extension member example (extension block syntax where available) to demonstrate a lightweight convenience member on the interface; see production notes about C# version compatibility.
Runnable example (in the codeExamples field) is a Console app targeting net10.0 that prints the sequence of operations for both Decorator and Proxy demonstrations.
Executable code examples
Decorator and Proxy demonstration (Console)
Program.cs
using System;
using System.Linq;
// Deterministic example demonstrating Decorator vs Proxy for IMessageSender
// Why this pattern is preferable to a simpler alternative:
// - Decorator: preferable to subclassing when you need to mix multiple orthogonal behaviors
// (logging + compression + metrics) without combinatorial explosion of subclasses.
// - Proxy: preferable to direct calls when you want transparent access control, lazy init, or
// virtualization without changing callers.
// Misuse warning: don't use Decorator to hide domain logic; don't rely on client-side Proxy as
// sole security enforcement in distributed systems.
public record Message(string User, string Payload);
public interface IMessageSender
{
void Send(Message request);
}
public class RealSender : IMessageSender
{
public void Send(Message request)
{
Console.WriteLine($"BaseSender: delivered '{request.Payload}'");
}
}
// Decorator: Logging
public class LoggingDecorator : IMessageSender
{
private readonly IMessageSender _inner;
public LoggingDecorator(IMessageSender inner) => _inner = inner;
public void Send(Message request)
{
Console.WriteLine($"LoggingDecorator: logging '{request.Payload}'");
_inner.Send(request);
}
}
// Decorator: Compression (deterministic simple vowel removal for demo)
public class CompressionDecorator : IMessageSender
{
private readonly IMessageSender _inner;
public CompressionDecorator(IMessageSender inner) => _inner = inner;
private static string Compress(string s)
{
// deterministic, trivial "compression": remove vowels
return string.Concat(s.Where(c => "aeiouAEIOU".IndexOf(c) < 0));
}
public void Send(Message request)
{
var compressed = Compress(request.Payload);
Console.WriteLine($"CompressionDecorator: compressing '{request.Payload}' -> '{compressed}'");
// Pass a new Message to avoid mutating caller data
_inner.Send(new Message(request.User, compressed));
}
}
// Proxy: Access control
public class AccessProxy : IMessageSender
{
private readonly IMessageSender _inner;
private readonly string _allowedUser;
public AccessProxy(IMessageSender inner, string allowedUser)
{
_inner = inner;
_allowedUser = allowedUser;
}
public void Send(Message request)
{
Console.WriteLine($"Proxy: checking access for '{request.User}'");
if (request.User == _allowedUser)
{
_inner.Send(request);
}
else
{
Console.WriteLine($"Proxy: access denied for '{request.User}'");
}
}
}
// NOTE on extension member: modern C# previews introduce an 'extension' block syntax for
// extension members. If your compiler supports it you can use it. To remain compatible,
// we provide a classic extension method below. In a C# 14+ environment, replace with an
// 'extension' block member if desired.
public static class SenderExtensions
{
// Convenience extension showing additional surface without modifying interface
public static void SendWithTrace(this IMessageSender sender, Message request, string traceId)
{
Console.WriteLine($"[trace:{traceId}] before send");
sender.Send(request);
}
}
class Program
{
static void Main()
{
Console.WriteLine("Decorator demo:");
// Stack decorators: Compression wraps Logging wraps RealSender
IMessageSender decorated = new CompressionDecorator(new LoggingDecorator(new RealSender()));
var msg = new Message("System", "Hello");
decorated.Send(msg);
Console.WriteLine();
Console.WriteLine("Proxy demo:");
IMessageSender proxied = new AccessProxy(new RealSender(), allowedUser: "Alice");
proxied.Send(new Message("Alice", "Hello"));
proxied.Send(new Message("Mallory", "Hello"));
// Demonstrate extension convenience
Console.WriteLine();
Console.WriteLine("Extension helper demo (SendWithTrace):");
decorated.SendWithTrace(new Message("System", "TraceExample"), traceId: "tx-123");
}
}