
Learn the most important C# design patterns with real code examples. Master Factory, Singleton, Observer, and Strategy patterns step by step.
C# Design Patterns Every Developer Must Know in 2026
C# design patterns are proven solutions to recurring software design problems. Whether you are building a small console application or a large enterprise system, understanding design patterns will make your code more maintainable, flexible, and scalable. In this tutorial, you will learn the four most essential design patterns in C# — Factory, Singleton, Observer, and Strategy — with practical code examples you can use in your projects right away.
Design patterns are not just academic concepts. They are battle-tested blueprints that thousands of professional developers use every day across companies like Microsoft, Google, and Amazon. The Gang of Four (GoF) originally catalogued 23 patterns in 1994, and they remain just as relevant in modern C# and .NET development.
Why Should You Learn C# Design Patterns?
Before diving into code, let us understand why design patterns matter for your career and your codebase:
- Shared vocabulary: When you say "let us use a Factory here," every developer on the team instantly understands the approach. This eliminates lengthy explanations and speeds up code reviews.
- Proven solutions: Patterns have been refined over decades. You avoid reinventing the wheel and introducing subtle bugs that others have already solved.
- SOLID principles in action: Design patterns naturally encourage Single Responsibility, Open/Closed, and Dependency Inversion principles — the foundations of clean C# architecture.
- Interview readiness: Design pattern questions appear in nearly every senior C# developer interview at top companies.
- Easier maintenance: Patterned code is easier to extend without breaking existing functionality, saving you hours of debugging.
1. Factory Pattern in C# — Creating Objects the Right Way
The factory pattern C# developers use most often solves a simple problem: how do you create objects without exposing the creation logic to the caller? Instead of scattering new statements throughout your code, you centralize object creation in one place.
When to Use the Factory Pattern
- You have multiple classes that share a common interface or base class
- The exact type of object to create is determined at runtime
- Object creation involves complex logic you do not want to repeat
Factory Pattern C# Example
Imagine a notification system that sends alerts through different channels:
public interface INotification
{
void Send(string message);
}
public class EmailNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Sending Email: {message}");
}
}
public class SmsNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Sending SMS: {message}");
}
}
public class PushNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Sending Push Notification: {message}");
}
}
public static class NotificationFactory
{
public static INotification Create(string channel)
{
return channel.ToLower() switch
{
"email" => new EmailNotification(),
"sms" => new SmsNotification(),
"push" => new PushNotification(),
_ => throw new ArgumentException($"Unknown channel: {channel}")
};
}
}
// Usage
var notification = NotificationFactory.Create("email");
notification.Send("Your order has been shipped!");
The caller never needs to know which concrete class is being instantiated. If you add a new notification channel next month, you only modify the factory — not every file that sends notifications.
Common Pitfall
Avoid creating a "god factory" that builds dozens of unrelated types. Each factory should have a single, clear responsibility. If your factory has more than five or six cases, consider splitting it or using an Abstract Factory instead.
2. Singleton Pattern in C# — One Instance, Guaranteed
The singleton pattern C# ensures a class has exactly one instance throughout your application's lifetime and provides a global point of access to it. This is ideal for shared resources like configuration managers, logging services, or database connection pools.
Thread-Safe Singleton in Modern C#
The simplest and most recommended approach in modern .NET uses the Lazy<T> class, which handles thread safety for you:
public sealed class AppLogger
{
private static readonly Lazy<AppLogger> _instance =
new(() => new AppLogger());
public static AppLogger Instance => _instance.Value;
private readonly List<string> _logs = new();
private AppLogger()
{
// Private constructor prevents external instantiation
}
public void Log(string message)
{
var entry = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] {message}";
_logs.Add(entry);
Console.WriteLine(entry);
}
public IReadOnlyList<string> GetLogs() => _logs.AsReadOnly();
}
// Usage — same instance everywhere
AppLogger.Instance.Log("Application started");
AppLogger.Instance.Log("Processing user request");
var allLogs = AppLogger.Instance.GetLogs();
Console.WriteLine($"Total log entries: {allLogs.Count}");
Singleton vs Dependency Injection
In modern .NET applications, you should prefer registering a service as a singleton through dependency injection over implementing the Singleton pattern manually:
// In Program.cs or Startup.cs
builder.Services.AddSingleton<IAppLogger, AppLogger>();
This gives you the same single-instance behavior while keeping your code testable and loosely coupled. Use the classic Singleton pattern only when dependency injection is not available, such as in console utilities or libraries where you do not control the DI container.
Common Pitfall
Singletons that hold mutable state can become a hidden source of bugs in multi-threaded applications. If multiple threads write to the singleton's state simultaneously, you will encounter race conditions. Always use locks or concurrent collections when your singleton stores shared data.
3. Observer Pattern in C# — Event-Driven Communication
The observer pattern C# defines a one-to-many relationship where one object (the subject) notifies all its dependents (observers) automatically when its state changes. C# has built-in support for this pattern through events and delegates, making it one of the most natural patterns to implement.
Observer Pattern Using C# Events
public class StockTicker
{
public event EventHandler<StockPriceChangedEventArgs>? PriceChanged;
private decimal _price;
public string Symbol { get; }
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
var oldPrice = _price;
_price = value;
OnPriceChanged(oldPrice, value);
}
}
}
public StockTicker(string symbol, decimal initialPrice)
{
Symbol = symbol;
_price = initialPrice;
}
protected virtual void OnPriceChanged(decimal oldPrice, decimal newPrice)
{
PriceChanged?.Invoke(this, new StockPriceChangedEventArgs
{
Symbol = Symbol,
OldPrice = oldPrice,
NewPrice = newPrice
});
}
}
public class StockPriceChangedEventArgs : EventArgs
{
public string Symbol { get; init; } = "";
public decimal OldPrice { get; init; }
public decimal NewPrice { get; init; }
}
public class TradingDashboard
{
public void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
var direction = e.NewPrice > e.OldPrice ? "UP" : "DOWN";
Console.WriteLine(
$"[Dashboard] {e.Symbol}: ${e.OldPrice} -> ${e.NewPrice} ({direction})");
}
}
public class AlertService
{
private readonly decimal _threshold;
public AlertService(decimal threshold) => _threshold = threshold;
public void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
var change = Math.Abs(e.NewPrice - e.OldPrice) / e.OldPrice * 100;
if (change >= _threshold)
{
Console.WriteLine(
$"[ALERT] {e.Symbol} moved {change:F1}% — check your portfolio!");
}
}
}
// Usage
var stock = new StockTicker("MSFT", 420.00m);
var dashboard = new TradingDashboard();
var alerts = new AlertService(threshold: 2.0m);
stock.PriceChanged += dashboard.OnPriceChanged;
stock.PriceChanged += alerts.OnPriceChanged;
stock.Price = 425.50m; // Both observers are notified
stock.Price = 440.00m; // Alert fires — over 2% change
This pattern is the backbone of every event-driven system in .NET. ASP.NET middleware, Blazor component updates, and Reactive Extensions (Rx) all build on this concept.
Common Pitfall
Forgetting to unsubscribe from events causes memory leaks. The subject holds a reference to every observer, preventing garbage collection. Always unsubscribe when the observer is no longer needed, or use weak event patterns in long-lived applications.
4. Strategy Pattern in C# — Swap Algorithms at Runtime
The strategy pattern C# lets you define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. The client code picks the algorithm it needs without knowing the implementation details.
Strategy Pattern C# Example
Consider an e-commerce system that applies different discount strategies:
public interface IDiscountStrategy
{
decimal CalculateDiscount(decimal originalPrice);
string Name { get; }
}
public class NoDiscount : IDiscountStrategy
{
public string Name => "No Discount";
public decimal CalculateDiscount(decimal originalPrice) => originalPrice;
}
public class PercentageDiscount : IDiscountStrategy
{
private readonly decimal _percent;
public string Name => $"{_percent}% Off";
public PercentageDiscount(decimal percent) => _percent = percent;
public decimal CalculateDiscount(decimal originalPrice)
=> originalPrice * (1 - _percent / 100);
}
public class BuyOneGetOneFree : IDiscountStrategy
{
public string Name => "Buy One Get One Free";
public decimal CalculateDiscount(decimal originalPrice)
=> originalPrice / 2;
}
public class ShoppingCart
{
private IDiscountStrategy _strategy = new NoDiscount();
public void SetDiscountStrategy(IDiscountStrategy strategy)
{
_strategy = strategy;
}
public void Checkout(decimal totalPrice)
{
var finalPrice = _strategy.CalculateDiscount(totalPrice);
Console.WriteLine($"Strategy: {_strategy.Name}");
Console.WriteLine($"Original: ${totalPrice:F2}");
Console.WriteLine($"Final: ${finalPrice:F2}");
Console.WriteLine($"You save: ${totalPrice - finalPrice:F2}");
Console.WriteLine();
}
}
// Usage — swap strategies at runtime
var cart = new ShoppingCart();
cart.Checkout(100.00m);
cart.SetDiscountStrategy(new PercentageDiscount(15));
cart.Checkout(100.00m);
cart.SetDiscountStrategy(new BuyOneGetOneFree());
cart.Checkout(100.00m);
Adding a new discount type (seasonal sale, loyalty reward, coupon code) requires zero changes to the ShoppingCart class. You simply create a new class that implements IDiscountStrategy and pass it in. This is the Open/Closed Principle in action.
Common Pitfall
Overusing the strategy pattern for logic that will never change leads to unnecessary complexity. If you only have two options and will never add a third, a simple if/else is clearer and easier to maintain than three new classes and an interface.
How to Choose the Right C# Design Pattern
Use this quick reference to pick the right pattern for your situation:
- Factory — Use when you need to create objects without specifying the exact class. Best for systems with multiple implementations of a shared interface.
- Singleton — Use when exactly one instance of a class must exist. Best for loggers, caches, configuration, and connection pools.
- Observer — Use when changes in one object must automatically update others. Best for event systems, UI updates, and real-time data feeds.
- Strategy — Use when you need to switch between algorithms at runtime. Best for payment processing, sorting, validation rules, and pricing logic.
Best Practices for Using Design Patterns in C#
- Do not force patterns where they are not needed. A pattern should simplify your code, not complicate it. If a straightforward approach works, use it.
- Combine patterns when it makes sense. A Factory can create Strategy objects. A Singleton can act as an Observer subject. Patterns compose naturally.
- Leverage C# language features. Use events for Observer,
Lazy<T>for Singleton, and switch expressions for Factory. Modern C# makes patterns cleaner than ever. - Prefer dependency injection. In ASP.NET Core and modern .NET, DI often replaces manual Singleton and Factory implementations with cleaner, testable alternatives.
- Write unit tests for each pattern. Patterns make testing easier — take advantage of that. Mock strategies, verify observer notifications, and test factory output.
Conclusion — Start Using C# Design Patterns Today
C# design patterns are not optional knowledge for professional developers — they are essential tools that separate junior code from production-ready architecture. In this tutorial, you learned the four most important patterns: Factory for flexible object creation, Singleton for shared resources, Observer for event-driven communication, and Strategy for interchangeable algorithms.
Start by identifying one place in your current project where you are duplicating creation logic or hard-coding algorithm choices. Refactor it using the appropriate pattern from this guide. Once you see the benefits firsthand — cleaner code, easier testing, and painless extensibility — you will naturally reach for these patterns in every project you build.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment