Design Patterns in C# (Factory, Singleton, Repository, Observer)
Learn design patterns in C#, including Factory, Singleton, Repository, and Observer, to build flexible and maintainable applications.
Design Patterns provide standard and proven solutions for recurring problems in software development. Commonly used patterns in C# include Factory, Singleton, Repository, Observer, Strategy, Adapter, and Decorator. This article explores the core concepts of these patterns, their use cases, and C# examples.
Factory Pattern
The Factory pattern centralizes object creation.
Instead of using new everywhere, the responsibility of creating objects is delegated to a factory class.
public interface IProduct { void DoWork(); }
public class ProductA : IProduct
{
public void DoWork() => Console.WriteLine("Product A");
}
public class ProductB : IProduct
{
public void DoWork() => Console.WriteLine("Product B");
}
public static class ProductFactory
{
public static IProduct Create(string type) => type switch
{
"A" => new ProductA(),
"B" => new ProductB(),
_ => throw new ArgumentException("Invalid type")
};
}
Singleton Pattern
The Singleton pattern ensures that only a single instance of a class is created. It is commonly used for logging, caching, or configuration objects.
public class Singleton
{
private static Singleton? _instance;
private static readonly object _lock = new();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (_lock)
return _instance ??= new Singleton();
}
}
}
Repository Pattern
The Repository pattern abstracts the data access layer. This way, the business logic layer becomes independent of database details.
public class Product { public int Id; public string Name = ""; }
public interface IProductRepository
{
void Add(Product p);
Product? GetById(int id);
}
public class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> _list = new();
public void Add(Product p) => _list.Add(p);
public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);
}
Observer Pattern
The Observer pattern establishes a publisher–subscriber relationship. When an object changes, all subscribers are notified. In C#, this is implemented with the event/delegate mechanism.
public class NewsAgency
{
public event EventHandler<string>? NewsPublished;
public void Publish(string news)
{
Console.WriteLine($"News: {news}");
NewsPublished?.Invoke(this, news);
}
}
Strategy Pattern
The Strategy pattern encapsulates algorithms in a way that allows them to be interchangeable. The client class does not need to know which algorithm is being used; the strategy is provided externally.
public interface IPaymentStrategy { void Pay(decimal amount); }
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount} USD paid by credit card.");
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount} USD paid via PayPal.");
}
public class Checkout
{
private readonly IPaymentStrategy _strategy;
public Checkout(IPaymentStrategy strategy) => _strategy = strategy;
public void ProcessOrder(decimal total) => _strategy.Pay(total);
}
Adapter Pattern
The Adapter pattern allows two incompatible classes to work together. It is often used to integrate legacy libraries or APIs into new systems.
// Old system
public class LegacyPrinter { public void PrintText(string t) => Console.WriteLine(t); }
// New interface
public interface IPrinter { void Print(string text); }
// Adapter
public class PrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacy;
public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
public void Print(string text) => _legacy.PrintText(text);
}
Decorator Pattern
The Decorator pattern allows adding new behaviors dynamically to existing objects. This is a composition-based approach rather than inheritance.
public interface IMessage { string GetText(); }
public class SimpleMessage : IMessage
{
private readonly string _text;
public SimpleMessage(string text) => _text = text;
public string GetText() => _text;
}
// Decorator
public class EncryptedMessage : IMessage
{
private readonly IMessage _inner;
public EncryptedMessage(IMessage inner) => _inner = inner;
public string GetText() => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(_inner.GetText()));
}
Advantages
- Factory: Abstracts object creation, increases flexibility.
- Singleton: Manages shared resources with a single instance guarantee.
- Repository: Abstracts data access, improves testability.
- Observer: Establishes publisher–subscriber relationships, ensuring loose coupling.
- Strategy: Makes algorithms interchangeable.
- Adapter: Integrates incompatible systems.
- Decorator: Allows adding new behavior dynamically.
TL;DR
- Factory: Abstracts object creation.
- Singleton: Guarantees a single instance.
- Repository: Abstracts data access.
- Observer: Notifies subscribers of changes.
- Strategy: Algorithms can be easily switched.
- Adapter: Adapts incompatible classes.
- Decorator: Dynamically adds features.
Example: Product Creation, Listing, Order Flow, and Logging
The sample below demonstrates product creation (Factory), listing (Repository),
order creation and status updates (Observer), payment (Strategy),
receipt printing (Adapter), and message processing (Decorator) together.
It uses safe singleton configuration with Lazy, an enum-based factory, custom EventArgs,
and culture-aware currency formatting.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
// =======================
// DOMAIN (Small model)
// =======================
// Express product types with an enum instead of strings to reduce errors.
public enum ProductType { Book, Phone }
// Simple data carrier (immutable) record type
public record Product(int Id, string Name, decimal Price);
// =======================
// REPOSITORY (Storage)
// =======================
public interface IProductRepository
{
void Add(Product p);
Product? GetById(int id);
IEnumerable<Product> GetAll(); // Expose IEnumerable; don't expose the inner list.
}
public class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> _list = new();
public void Add(Product p) => _list.Add(p);
public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);
// If you want to return a protective copy: return _list.ToList();
public IEnumerable<Product> GetAll() => _list;
}
// =======================
// FACTORY (Product creation)
// =======================
public static class ProductFactory
{
// Centralized creation: when adding a new type, just extend the switch.
public static Product Create(ProductType type, int id)
=> type switch
{
ProductType.Book => new Product(id, $"Book-{id}", 29.90m),
ProductType.Phone => new Product(id, $"Phone-{id}", 599.00m),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
}
// =======================
// SINGLETON SETTINGS
// =======================
// Thread-safe, lazy-loaded, simple Singleton via Lazy<T>.
public sealed class AppSettings
{
private static readonly Lazy<AppSettings> _lazy = new(() => new AppSettings());
public static AppSettings Instance => _lazy.Value;
private AppSettings() { }
public string StoreName { get; init; } = "My Store";
}
// =======================
// OBSERVER (Event Model)
// =======================
// Custom EventArgs: a structure that is extensible instead of plain string.
public sealed class OrderStatusChangedEventArgs : EventArgs
{
public string Status { get; }
public DateTime When { get; }
public OrderStatusChangedEventArgs(string status, DateTime when)
{
Status = status;
When = when;
}
}
public class Order
{
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
public int Id { get; }
public Product Product { get; }
public Order(int id, Product product)
{
Id = id;
Product = product;
}
public void UpdateStatus(string status)
{
Console.WriteLine($"Order {Id} status: {status}");
StatusChanged?.Invoke(this, new OrderStatusChangedEventArgs(status, DateTime.Now));
}
}
// Event listeners (subscribers)
public class EmailNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
=> Console.WriteLine($"[Email] Notification sent to customer: {e.Status} ({e.When:T})");
}
public class SmsNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
=> Console.WriteLine($"[SMS] Notification sent to customer: {e.Status} ({e.When:T})");
}
// =======================
// STRATEGY (Payment)
// =======================
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} paid by credit card.");
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} paid via PayPal.");
}
// =======================
// ADAPTER (Receipt Printing)
// =======================
// Legacy API
public class LegacyPrinter
{
public void PrintText(string t) => Console.WriteLine("[LegacyPrinter] " + t);
}
// New interface
public interface IPrinter
{
void Print(string text);
}
// Adapter: Connects the new interface to the legacy API.
public class PrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacy;
public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
public void Print(string text) => _legacy.PrintText(text);
}
// =======================
// DECORATOR (Message)
// =======================
public interface IMessage
{
string GetText();
}
public class SimpleMessage : IMessage
{
private readonly string _text;
public SimpleMessage(string text) => _text = text;
public string GetText() => _text;
}
// Decorator that logs the message
public class LoggedMessageDecorator : IMessage
{
private readonly IMessage _inner;
public LoggedMessageDecorator(IMessage inner) => _inner = inner;
public string GetText()
{
var text = _inner.GetText();
var preview = text.Length > 60 ? text.Substring(0, 60) + "..." : text;
Console.WriteLine("[Log] Message created (preview): " + preview);
return text;
}
}
// Simple decorator that "encrypts" the message with Base64
public class EncryptedMessageDecorator : IMessage
{
private readonly IMessage _inner;
public EncryptedMessageDecorator(IMessage inner) => _inner = inner;
public string GetText()
{
var raw = _inner.GetText();
return Convert.ToBase64String(Encoding.UTF8.GetBytes(raw));
}
}
// =======================
// APPLICATION (Demo Flow)
// =======================
class Program
{
static void Main()
{
// Currency formatting with en-US culture
var enUS = new CultureInfo("en-US");
Console.WriteLine($"Welcome to: {AppSettings.Instance.StoreName}");
Console.WriteLine();
// Add products via Repository + Factory
IProductRepository repo = new InMemoryProductRepository();
var p1 = ProductFactory.Create(ProductType.Book, 1);
var p2 = ProductFactory.Create(ProductType.Phone, 2);
repo.Add(p1);
repo.Add(p2);
// List products (culture-aware price)
Console.WriteLine("Products in stock:");
foreach (var p in repo.GetAll())
Console.WriteLine($"- {p.Id}: {p.Name} {p.Price.ToString("C", enUS)}");
Console.WriteLine();
// Create order and subscribe to events
var order = new Order(1001, p1);
var email = new EmailNotifier();
var sms = new SmsNotifier();
// Add listeners to events
order.StatusChanged += email.OnOrderStatusChanged;
order.StatusChanged += sms.OnOrderStatusChanged;
// Payment (Strategy)
IPaymentStrategy payment = new CreditCardPayment();
Console.WriteLine("Processing payment:");
payment.Pay(order.Product.Price);
Console.WriteLine();
// Order lifecycle (Observer gets triggered)
order.UpdateStatus("Approved");
order.UpdateStatus("Shipped");
// (Optional) Unsubscribe SMS listener
order.StatusChanged -= sms.OnOrderStatusChanged;
order.UpdateStatus("Delivered");
Console.WriteLine();
// Print receipt (use legacy printer API through Adapter)
IPrinter printer = new PrinterAdapter(new LegacyPrinter());
printer.Print($"Receipt for Order #{order.Id}: {order.Product.Name} - {order.Product.Price.ToString("C", enUS)}");
Console.WriteLine();
// Message processing (Decorator)
// Goal: Log plain text, then encrypt it before storing/sending.
// Therefore, chain Logged first, then Encrypted decorator.
IMessage msg = new SimpleMessage($"Thank you for your purchase: {order.Product.Name}!");
msg = new LoggedMessageDecorator(msg); // 1) Log: LOG THE PLAIN TEXT
msg = new EncryptedMessageDecorator(msg); // 2) Encrypt: Output encrypted text
var finalText = msg.GetText();
Console.WriteLine("Final message to store/send (Base64):");
Console.WriteLine(finalText);
Console.WriteLine();
// Switch strategy at runtime
payment = new PayPalPayment();
Console.WriteLine("Second payment with a different strategy:");
payment.Pay(199.99m);
Console.WriteLine("\nDemo completed.");
}
}
Related Articles
Dependency Injection Basics in C#
Learn the basics of Dependency Injection in C#, including managing dependencies, loose coupling, and improving testability.
Encapsulation, Inheritance, and Polymorphism in C#
Learn encapsulation, inheritance, and polymorphism in C# with clear examples to understand core OOP principles and real use cases.
Interfaces and Abstract Classes in C#
Learn interfaces and abstract classes in C#, their differences, and when to use each approach to design clean and extensible code.
SOLID Principles with C#
Applying SOLID principles in C# with examples: building flexible, maintainable, and testable code.