Skip to main content

C# Design Patterns Tutorial: Factory, Singleton & More

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.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...