In modern software development, we are constantly told to follow the S.O.L.I.D principles. But in the real world, requirements grow, and classes quickly become “God Objects”—bloated files that handle business logic, logging, security, and caching all at once.
How do we add these cross-cutting concerns without turning our codebase into a maintenance nightmare? The answer is the Decorator Design Pattern.
What is the Decorator Design Pattern?
The Decorator is a structural design pattern that allows you to attach new behaviors to objects by placing these objects inside special wrapper objects.
Think of it as an onion. The core of the onion is your business logic. Each layer you wrap around it adds a new responsibility (Validation, Logging, Caching) without changing the core itself.
Why Use It? (The Benefits)
- Single Responsibility Principle (SRP): You can move operational logic (like logging) out of the business service.
- Open/Closed Principle: You can add new features to a service without modifying its existing code.
- Composable Logic: You can mix and match behaviors at runtime (e.g., wrap a service with logging only in “Debug” mode).
- Avoids Inheritance Bloat: Instead of creating a
LoggingCachedCustomerServicesubclass, you simply wrap your components.
The Problem: Life Without Decorators
Imagine a standard Customer Service class. As the project grows, it starts to look like this “spaghetti” service:
public class CustomerService
{
public Customer GetCustomer(int id)
{
// 1. Validation logic
if (id <= 0) throw new ArgumentException("Invalid ID");
// 2. Caching logic
if (_cache.Contains(id)) return _cache.Get(id);
// 3. Logging logic
Console.WriteLine($"Fetching customer {id} from database...");
// 4. Core Business Logic (The only part that SHOULD be here)
var customer = _db.Customers.Find(id);
// 5. Save to Cache
_cache.Add(id, customer);
return customer;
}
}
This class is hard to test. If you want to test the database logic, you’re forced to deal with caching and logging code.
The Solution: Elegant Decoration
In the Decorator pattern, we create an interface that all layers (including the core service) implement.
1. The Core Infrastructure
First, we define our interface and the “Core” service that does exactly one thing: gets data from the database.
public record Customer(int Id, string Name);
public interface ICustomerService
{
Customer GetCustomer(int id);
}
public class CustomerService : ICustomerService
{
public Customer GetCustomer(int id)
{
// Pure business logic: Just get the data
return new Customer(id, "John Doe");
}
}
2. Creating the Decorator Layers
Each decorator implements the interface and accepts an instance of the interface in its constructor.
// VALIDATION DECORATOR
public class CustomerValidationDecorator : ICustomerService
{
private readonly ICustomerService _inner;
public CustomerValidationDecorator(ICustomerService inner) => _inner = inner;
public Customer GetCustomer(int id)
{
if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id), "ID must be positive.");
return _inner.GetCustomer(id);
}
}
// LOGGING DECORATOR
public class CustomerLoggingDecorator : ICustomerService
{
private readonly ICustomerService _inner;
public CustomerLoggingDecorator(ICustomerService inner) => _inner = inner;
public Customer GetCustomer(int id)
{
Console.WriteLine($"[LOG]: Fetching customer {id}...");
var result = _inner.GetCustomer(id);
Console.WriteLine($"[LOG]: Successfully retrieved customer {id}.");
return result;
}
}
3. Assembling the Application
You can now stack these layers like Lego bricks. The order determines the flow of execution.
// We wrap the core service in Logging, then wrap that in Validation
ICustomerService service =
new CustomerValidationDecorator( new CustomerLoggingDecorator( new CustomerService() ));
// When called, it goes: Validation -> Logging -> Core Service
var customer = service.GetCustomer(1);
Visualizing the Data Flow
When a request comes in, it travels through the decorators from the outside in. If a decorator decides to return early (like a Cache hit or a Validation failure), the inner layers are never even executed!

Summary
The Decorator pattern is one of the best ways to keep your C# code Clean. Instead of writing one massive service that handles every possible scenario, you write small, focused classes that do one thing well.
The next time you find yourself adding if (loggingEnabled) or if (cacheHit) inside your business logic, stop and consider: Is it time for a Decorator?