π§ Mastering 5 Design Patterns in C# - Part 1


β¨ Introduction
Design patterns are reusable solutions to common software design problems. In this article, weβll explore 5 essential patterns using the same scenario for easy comparison: a notification system (email, SMS, push). The patterns weβll cover are:
Singleton
Factory Method
Observer
Decorator
Strategy
Each pattern includes code, purpose, pros and cons, and line-by-line explanations.
π 1. Singleton β Notification Manager
β Intent
Ensure a class has only one instance and provide a global point of access to it.
π― Purpose
Best used when you need a single point of control for a shared resource, such as a configuration manager, logging service, or notification dispatcher.
π§© Code
public class NotificationManager
{
private static NotificationManager _instance;
private NotificationManager() { }
public static NotificationManager Instance
{
get
{
if (_instance == null)
_instance = new NotificationManager();
return _instance;
}
}
public void Send(string message)
{
Console.WriteLine($"Sending: {message}");
}
}
π§ Explanation
private static NotificationManager _instance;
β Stores the single instance.private NotificationManager()
β Prevents external instantiation.Instance
β Returns the instance, creating it if needed.Send()
β Sends a notification.
π Pros
Centralized control.
Easy global access.
π Cons
Harder to test (mocking).
Can lead to tight coupling.
π 2. Factory Method β Notification Creation
β Intent
Define an interface for creating an object, but let subclasses decide which class to instantiate.
π― Purpose
Useful when your system needs to create objects based on dynamic conditions, like notification type, without exposing instantiation logic.
π§© Code
public abstract class Notification
{
public abstract void Send(string message);
}
public class EmailNotification : Notification
{
public override void Send(string message)
{
Console.WriteLine($"Email: {message}");
}
}
public class SmsNotification : Notification
{
public override void Send(string message)
{
Console.WriteLine($"SMS: {message}");
}
}
public class NotificationFactory
{
public static Notification Create(string type)
{
return type switch
{
"email" => new EmailNotification(),
"sms" => new SmsNotification(),
_ => throw new ArgumentException("Invalid type")
};
}
}
π§ Explanation
Notification
β Abstract base class.EmailNotification
andSmsNotification
β Concrete implementations.NotificationFactory.Create()
β Creates instance based on type.
π Pros
Easy to add new types.
Encapsulates creation logic.
π Cons
Can grow large with many types.
Factory maintenance required.
π 3. Observer β Real-Time Notifications
β Intent
Define a one-to-many dependency so that when one object changes state, all its dependents are notified.
π― Purpose
Ideal for event-driven systems, such as GUIs, real-time notifications, or publish/subscribe architectures.
π§© Code
public interface IObserver
{
void Update(string message);
}
public class EmailObserver : IObserver
{
public void Update(string message)
{
Console.WriteLine($"Email received: {message}");
}
}
public class SmsObserver : IObserver
{
public void Update(string message)
{
Console.WriteLine($"SMS received: {message}");
}
}
public class NotificationPublisher
{
private List<IObserver> observers = new();
public void Subscribe(IObserver observer) => observers.Add(observer);
public void Unsubscribe(IObserver observer) => observers.Remove(observer);
public void NotifyAll(string message)
{
foreach (var observer in observers)
observer.Update(message);
}
}
π§ Explanation
IObserver
β Interface for observers.EmailObserver
andSmsObserver
β Implementations.NotificationPublisher
β Manages and notifies observers.
π Pros
Loose coupling.
Easy to extend.
π Cons
Can be hard to debug.
Notification order not guaranteed
π¨ 4. Decorator β Adding Behavior
β Intent
Attach additional responsibilities to an object dynamically.
π― Purpose
Perfect when you need to extend functionality dynamically, such as adding logging, compression, or authentication to services.
π§© Code
public interface INotifier
{
void Send(string message);
}
public class BasicNotifier : INotifier
{
public void Send(string message)
{
Console.WriteLine($"Basic: {message}");
}
}
public class EmailDecorator : INotifier
{
private readonly INotifier _notifier;
public EmailDecorator(INotifier notifier)
{
_notifier = notifier;
}
public void Send(string message)
{
_notifier.Send(message);
Console.WriteLine($"Email extra: {message}");
}
}
π§ Explanation
INotifier
β Base interface.BasicNotifier
β Default behavior.EmailDecorator
β Adds email functionality.
π Pros
Extensible without inheritance.
Flexible composition.
π Cons
Can become complex with many decorators.
Order of application matters.
π§ 5. Strategy β Interchangeable Sending Algorithms
β Intent
Define a family of algorithms, encapsulate each one, and make them interchangeable.
π― Purpose
Best used when there are multiple ways to perform a task, such as different sending methods, shipping calculations, or authentication strategies.
π§© Code
public interface ISendStrategy
{
void Send(string message);
}
public class EmailStrategy : ISendStrategy
{
public void Send(string message)
{
Console.WriteLine($"Email strategy: {message}");
}
}
public class SmsStrategy : ISendStrategy
{
public void Send(string message)
{
Console.WriteLine($"SMS strategy: {message}");
}
}
public class NotificationContext
{
private ISendStrategy _strategy;
public NotificationContext(ISendStrategy strategy)
{
_strategy = strategy;
}
public void Execute(string message)
{
_strategy.Send(message);
}
}
π§ Explanation
ISendStrategy
β Strategy interface.EmailStrategy
andSmsStrategy
β Concrete strategies.NotificationContext
β Executes selected strategy.
π Pros
Interchangeable algorithms.
Low coupling.
π Cons
Requires more classes.
Overkill for simple cases.
π§Ύ Conclusion
Each design pattern solves a different problem. Using the same notification example, we saw how each one approaches the challenge uniquely:
Singleton centralizes control.
Factory Method abstracts creation.
Observer enables multi-receiver updates.
Decorator adds behavior dynamically.
Strategy swaps algorithms flexibly.
The added Purpose section helps clarify when to use each pattern in real-world scenarios.
#CSharp #DesignPatterns #ObjectOrientedProgramming #DotNet #CleanCode #SoftwareArchitecture #DevLife #BackendDevelopment #Singleton #FactoryMethod #Observer #Decorator #Strategy #development
Subscribe to my newsletter
Read articles from Johnny Hideki Kinoshita de Faria directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Johnny Hideki Kinoshita de Faria
Johnny Hideki Kinoshita de Faria
Technology professional with over 15 years of experience delivering innovative, scalable, and secure solutions β especially within the financial sector. I bring deep expertise in Oracle PL/SQL (9+ years), designing robust data architectures that ensure performance and reliability. On the back-end side, Iβve spent 6 years building enterprise-grade applications using .NET, applying best practices like TDD and clean code to deliver high-quality solutions. In addition to my backend strengths, I have 6 years of experience with PHP and JavaScript, allowing me to develop full-stack web applications that combine strong performance with intuitive user interfaces. I've led and contributed to projects involving digital account management, integration of VISA credit and debit transactions, modernization of payment systems, financial analysis tools, and fraud prevention strategies. Academically, I hold a postgraduate certificate in .NET Architecture and an MBA in IT Project Management, blending technical skill with business acumen. Over the past 6 years, Iβve also taken on leadership roles β managing teams, mentoring developers, and driving strategic initiatives. I'm fluent in agile methodologies and make consistent use of tools like Azure Boards to coordinate tasks and align team performance with delivery goals.