C# Design Patterns (Intermediate): Practical Patterns, Use Cases, and Alternatives
At the intermediate level, design patterns focus more on flexibility, extensibility, and separation of concerns. These patterns are widely used in real-world applications and often appear in frameworks and enterprise systems.
Selected Intermediate Design Patterns
We’ll cover the following:
• Strategy Pattern
• Repository Pattern
• Decorator Pattern
• Command Pattern
1. Strategy Pattern
Purpose: Defines a family of algorithms and allows them to be interchangeable at runtime.
When to Use
• Multiple ways to perform a task
• Avoiding large conditional statements
Example
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paid {amount} using Credit Card");
}
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paid {amount} using PayPal");
}
}
public class PaymentContext
{
private IPaymentStrategy _strategy;
public void SetStrategy(IPaymentStrategy strategy)
{
_strategy = strategy;
}
public void ExecutePayment(decimal amount)
{
_strategy.Pay(amount);
}
}
Usage
var context = new PaymentContext();
context.SetStrategy(new CreditCardPayment());
context.ExecutePayment(100);
context.SetStrategy(new PayPalPayment());
context.ExecutePayment(200);
Advantages of Strategy Pattern
• Eliminates conditional logic
• Promotes Open/Closed Principle
• Easy to extend with new strategies
Alternatives of Strategy Pattern
• Simple switch-case (less flexible)
• Delegates or Func (lighter-weight approach in C#)
2. Repository Pattern
Purpose: Abstracts data access logic and provides a clean separation between business logic and data layer.
When to Use
• Applications with database interaction
• Implementing clean architecture
Example
public interface IRepository
{
IEnumerable GetAll();
T GetById(int id);
void Add(T entity);
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ProductRepository : IRepository
{
private readonly List _products = new List();
public IEnumerable GetAll() => _products;
public Product GetById(int id) =>
_products.FirstOrDefault(p => p.Id == id);
public void Add(Product entity)
{
_products.Add(entity);
}
}
Usage
var repo = new ProductRepository();
repo.Add(new Product { Id = 1, Name = "Laptop" });
var product = repo.GetById(1);
Console.WriteLine(product.Name);
Advantages of Repository Pattern
• Decouples business logic from data access
• Easier to test (mock repositories)
• Centralized data logic
Alternatives of Repository Pattern
• Direct use of ORM (like Entity Framework DbContext)
• CQRS for more complex systems
3. Decorator Pattern
Purpose: Adds behavior to objects dynamically without modifying their structure.
When to Use
• Extending functionality without inheritance
• Adding responsibilities at runtime
Example
public interface IMessage
{
string GetContent();
}
public class SimpleMessage : IMessage
{
public string GetContent() => "Hello";
}
public abstract class MessageDecorator : IMessage
{
protected IMessage _message;
public MessageDecorator(IMessage message)
{
_message = message;
}
public abstract string GetContent();
}
public class ExclamationDecorator : MessageDecorator
{
public ExclamationDecorator(IMessage message) : base(message) { }
public override string GetContent()
{
return _message.GetContent() + "!";
}
}
Usage
IMessage message = new SimpleMessage();
message = new ExclamationDecorator(message);
Console.WriteLine(message.GetContent()); // Hello!
Advantages of Decorator Pattern
• Flexible alternative to inheritance
• Follows Open/Closed Principle
• Combine multiple behaviors
Alternatives of Decorator Pattern
• Inheritance (less flexible)
• Extension methods (limited use cases)
4. Command Pattern
Purpose: Encapsulates a request as an object, allowing you to parameterize and queue operations.
When to Use
• Undo/redo functionality
• Task scheduling
• Decoupling sender and receiver
Example
public interface ICommand
{
void Execute();
}
public class Light
{
public void TurnOn() => Console.WriteLine("Light is ON");
}
public class TurnOnCommand : ICommand
{
private readonly Light _light;
public TurnOnCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOn();
}
}
public class RemoteControl
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
}
}
Usage
var light = new Light();
var command = new TurnOnCommand(light);
var remote = new RemoteControl();
remote.SetCommand(command);
remote.PressButton();
Advantages of Command Pattern
• Decouples invoker from receiver
• Supports undo/redo
• Enables command queuing
Alternatives of Command Pattern
• Direct method calls (simpler but tightly coupled)
• Delegates/Action (lighter approach in C#)
Summary Table
| Pattern | Purpose | Best Use Case | Alternative |
|---|---|---|---|
| Strategy | Interchangeable algorithms | Payment systems, sorting | Switch-case, delegates |
| Repository | Data access abstraction | Database-driven apps | ORM direct usage |
| Decorator | Dynamic behavior extension | UI, logging, streams | Inheritance |
| Command | Encapsulate requests | Undo/redo, task queues | Direct calls, delegates |
Closing Thoughts
Intermediate design patterns help you move beyond basic structure into real-world architecture decisions. Mastering these patterns allows you to build systems that are easier to scale, test, and maintain.