🧠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 and SmsNotification β†’ 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 and SmsObserver β†’ 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 and SmsStrategy β†’ 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

0
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.