Have you ever looked at a codebase and noticed the same structural problem showing up in different places? Design patterns are reusable solutions to those recurring problems. They give you a shared vocabulary with other developers and a starting point for writing code that’s easier to maintain.
I picked three patterns for this post because they’re the ones I’ve reached for most often over the course of my career: Singleton, Factory Method, and Observer. Each one solves a different kind of problem, and each one has tradeoffs worth understanding before you use it.
Singleton Pattern
The Singleton pattern restricts a class to a single instance and provides a global access point to it. You’d use this for things like a configuration manager or a logging service where having multiple instances would cause problems.
Here’s the classic hand-rolled version in C#:
public class AppSettings
{
private static AppSettings _instance;
private static readonly object _lock = new object();
private AppSettings() { }
public static AppSettings Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new AppSettings();
}
return _instance;
}
}
}
public string ConnectionString { get; set; }
}
However, I should be honest about this: I rarely write Singletons this way anymore. The hand-rolled approach hides dependencies, makes unit testing harder, and introduces global state that can be difficult to reason about.
In modern .NET, the built-in dependency injection container handles singleton lifetime for you. You register the service once, and the container ensures there’s only one instance:
builder.Services.AddSingleton<IAppSettings, AppSettings>();
This gives you the same single-instance behavior while keeping your code testable and your dependencies explicit. In my experience, if you find yourself writing a manual Singleton in .NET today, it’s worth asking whether the DI container could manage that lifetime instead.
Factory Method Pattern
When you need to create different types of objects without tying your calling code to specific classes, the Factory Method pattern is useful. It defines an interface for creating objects and lets subclasses decide which concrete type to instantiate.
I saw this pattern pay off on a project where we needed to support multiple notification channels. The calling code didn’t need to know whether it was creating an email notification, an SMS notification, or a push notification. It just asked the factory:
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 abstract class NotificationFactory
{
public abstract INotification CreateNotification();
}
public class EmailNotificationFactory : NotificationFactory
{
public override INotification CreateNotification()
{
return new EmailNotification();
}
}
public class SmsNotificationFactory : NotificationFactory
{
public override INotification CreateNotification()
{
return new SmsNotification();
}
}
The value here is that when we later added push notifications, we created a new factory and a new notification class. The existing code didn’t change. That’s the open/closed principle in practice.
In modern C#, you can also achieve this with generics or with DI registration, depending on how dynamic the object creation needs to be. The Factory Method pattern is still the right choice when creation logic is complex or varies significantly between types.
Observer Pattern
The Observer pattern sets up a relationship where one object (the “subject”) notifies a list of dependent objects (“observers”) when its state changes. This is the foundation of most event-driven systems.
I first started appreciating this pattern when building systems where multiple components needed to react to the same state change. Instead of having the source know about every consumer, the Observer pattern inverts that relationship.
Here’s the classic implementation:
public interface IStockObserver
{
void Update(string symbol, decimal price);
}
public class StockTicker
{
private readonly List<IStockObserver> _observers = new List<IStockObserver>();
private decimal _price;
public void Subscribe(IStockObserver observer)
{
_observers.Add(observer);
}
public void Unsubscribe(IStockObserver observer)
{
_observers.Remove(observer);
}
public void UpdatePrice(string symbol, decimal price)
{
_price = price;
foreach (var observer in _observers)
{
observer.Update(symbol, price);
}
}
}
public class PriceAlert : IStockObserver
{
public void Update(string symbol, decimal price)
{
Console.WriteLine($"Alert: {symbol} is now ${price}");
}
}
C# has built-in language features that implement this pattern for you. The event keyword and EventHandler delegate are the idiomatic way to do this in C#:
public class StockTicker
{
public event EventHandler<StockPriceChangedEventArgs> PriceChanged;
public void UpdatePrice(string symbol, decimal price)
{
PriceChanged?.Invoke(this, new StockPriceChangedEventArgs(symbol, price));
}
}
For more complex reactive scenarios, IObservable<T> and IObserver<T> (part of .NET’s base class library, extended by Reactive Extensions) give you filtering, transformation, and composition on top of the observer pattern.
Understanding the manual implementation helps you see what event and IObservable<T> are doing under the hood. But in production C# code, I’d default to the built-in mechanisms.
A Word of Caution
The biggest mistake I see with design patterns is reaching for one before fully understanding the problem. Patterns are solutions, not goals. If a simpler approach works, use it. The Gang of Four, who originally cataloged these patterns, made this point explicitly: apply a pattern only when the flexibility it provides is actually needed.
In my experience, patterns are most valuable as a shared vocabulary. When a teammate says “we should use a Factory here” or “this is an Observer,” everyone immediately understands the structure being proposed. That communication benefit is at least as valuable as the code structure itself.
Resources
- Refactoring Guru: Design Patterns — clear explanations with visual diagrams for all 22 GoF patterns
- .NET Dependency Injection — Microsoft’s docs on the built-in DI container
- Design Patterns: Elements of Reusable Object-Oriented Software — the original Gang of Four book

