Singleton
Goal: Ensure a class has a single instance and provide a global access point. Typical for configuration, logging, process-wide cache.
C# (thread-safe, lazy)
public sealed class AppLogger {
private static readonly Lazy<AppLogger> _lazy = new(() => new AppLogger());
public static AppLogger Instance => _lazy.Value;
private AppLogger() { }
public void Log(string message) => Console.WriteLine($"[{DateTime.UtcNow:o}] {message}");
}
// Usage
AppLogger.Instance.Log("Started");
- A resource must be unique (e.g., a process-wide scheduler).
- You need lazy initialization + thread safety.
- Hidden coupling & global state complicate tests.
- Prefer DI containers for most app services.
Factory
Goal: Encapsulate object creation so callers depend on abstractions, not concrete classes.
C# (Simple Factory)
public interface IVehicle { void Drive(); }
public class Car : IVehicle { public void Drive() => Console.WriteLine("Car"); }
public class Motorcycle : IVehicle { public void Drive() => Console.WriteLine("Motorcycle"); }
public static class VehicleFactory {
public static IVehicle Create(string type) => type.ToLower() switch {
"car" => new Car(),
"motorcycle" => new Motorcycle(),
_ => throw new ArgumentException("Unknown type")
};
}
// Usage
var v = VehicleFactory.Create("car"); v.Drive();
- Construction logic is non-trivial (validation, caching).
- You want to return different implementations by input/flags.
- Large
switch
blocks—extract to Factory Method/DI.
Builder
Goal: Construct complex objects step-by-step and keep construction separate from representation.
C# (Fluent Builder)
public record Report(string Title, string Body, string Footer);
public class ReportBuilder {
private string _title = "Untitled";
private string _body = "";
private string _footer = "";
public ReportBuilder Title(string t) { _title = t; return this; }
public ReportBuilder Body(string b) { _body = b; return this; }
public ReportBuilder Footer(string f) { _footer = f; return this; }
public Report Build() => new(_title,_body,_footer);
}
// Usage
var report = new ReportBuilder()
.Title("Q2 Results")
.Body("Revenue grew 12%...")
.Footer("© Company")
.Build();
- Many optional parts/constraints.
- Different “recipes” for the same product.
- Too many builders—consider object initializer if simple.
Prototype
Goal: Create new objects by copying an existing instance (the prototype), useful when construction is expensive.
C# (shallow clone via MemberwiseClone()
)
public interface IPrototype<T> { T Clone(); }
public class Widget : IPrototype<Widget> {
public int Id { get; set; }
public List<string> Tags { get; set; } = new();
public Widget(int id) { Id = id; }
public Widget Clone() => (Widget)this.MemberwiseClone(); // shallow
}
// Usage
var p = new Widget(1); p.Tags.Add("blue");
var c = p.Clone(); c.Id = 2; c.Tags.Add("new"); // shares Tags with p!
- Manual copy (new instances for nested objects).
- Serialization / deserialization (for serializable graphs).
Choose deep clone if child objects must not be shared between copies.
Adapter
Goal: Convert one interface into another clients expect, enabling classes to work together despite incompatible interfaces.
C# (object adapter)
public interface ITarget { string Request(); }
public class Adaptee { public string SpecificRequest() => "legacy result"; }
public class Adapter : ITarget {
private readonly Adaptee _adaptee;
public Adapter(Adaptee a) => _adaptee = a;
public string Request() => _adaptee.SpecificRequest().ToUpperInvariant();
}
// Usage
ITarget api = new Adapter(new Adaptee());
Console.WriteLine(api.Request());
- Integrate legacy libraries without modifying them.
- Present a uniform interface to multiple providers.
Observer
Goal: Define a one-to-many dependency so that when the subject changes state, all observers are notified.
C# (manual observer)
public interface IObserver { void Update(string msg); }
public interface ISubject { void Attach(IObserver o); void Detach(IObserver o); void Notify(string msg); }
public class NewsFeed : ISubject {
private readonly List<IObserver> _subs = new();
public void Attach(IObserver o) => _subs.Add(o);
public void Detach(IObserver o) => _subs.Remove(o);
public void Notify(string msg) { foreach (var s in _subs) s.Update(msg); }
}
public class Subscriber : IObserver {
public string Name { get; }
public Subscriber(string name) => Name = name;
public void Update(string msg) => Console.WriteLine($"{Name} got: {msg}");
}
// Usage
var feed = new NewsFeed();
var a = new Subscriber("Alice"); var b = new Subscriber("Bob");
feed.Attach(a); feed.Attach(b);
feed.Notify("New article published!");
- Unsubscribe to avoid leaks (or use weak events/Rx).
- .NET also offers
event
/delegate
for built-in pub/sub.
Strategy
Goal: Define a family of algorithms, encapsulate each one, and make them interchangeable.
C# (pricing strategies)
public interface IPricing { decimal PriceFor(decimal basePrice); }
public class NoDiscount : IPricing { public decimal PriceFor(decimal p) => p; }
public class TenPercentOff : IPricing { public decimal PriceFor(decimal p) => p * 0.9m; }
public class Product {
public string Name { get; }
public decimal BasePrice { get; }
private IPricing _strategy;
public Product(string name, decimal price, IPricing s) { Name=name; BasePrice=price; _strategy=s; }
public decimal Total() => _strategy.PriceFor(BasePrice);
}
// Usage
var a = new Product("Mouse", 100m, new NoDiscount());
var b = new Product("Keyboard", 100m, new TenPercentOff());
Console.WriteLine(a.Total()); Console.WriteLine(b.Total());
- Multiple interchangeable algorithms (sorts, pricing, routing).
- Avoid giant
if/else
trees.
Decorator
Goal: Attach additional responsibilities to an object dynamically by wrapping it, instead of subclassing.
C# (logging decorator)
public interface IRepository { Task<T?> Get<T>(string id); }
public class SqlRepository : IRepository {
public async Task<T?> Get<T>(string id) { await Task.Delay(10); return default; }
}
public class LoggingRepoDecorator : IRepository {
private readonly IRepository _inner;
public LoggingRepoDecorator(IRepository inner) => _inner = inner;
public async Task<T?> Get<T>(string id) {
Console.WriteLine($"Fetching {id}...");
var result = await _inner.Get<T>(id);
Console.WriteLine($"Done {id}.");
return result;
}
}
// Usage
IRepository repo = new LoggingRepoDecorator(new SqlRepository());
await repo.Get<object>("42");
- Add cross-cutting concerns (logging, caching, retry) per instance.
- Turn features on/off without subclass explosion.
Chain of Responsibility
Goal: Pass a request along a chain of handlers; each can handle it or pass it on.
C# (purchase approvals)
public record Purchase(string Item, decimal Amount);
public interface IHandler {
IHandler SetNext(IHandler next);
string Handle(Purchase p);
}
public abstract class HandlerBase : IHandler {
private IHandler? _next;
public IHandler SetNext(IHandler next) { _next = next; return next; }
public virtual string Handle(Purchase p) => _next?.Handle(p) ?? "Rejected";
}
public class Employee : HandlerBase {
public override string Handle(Purchase p) => p.Amount <= 100 ? "Employee: Approved" : base.Handle(p);
}
public class Manager : HandlerBase {
public override string Handle(Purchase p) => p.Amount <= 1000 ? "Manager: Approved" : base.Handle(p);
}
public class Director : HandlerBase {
public override string Handle(Purchase p) => p.Amount <= 10000 ? "Director: Approved" : "Director: Rejected";
}
// Usage
var chain = new Employee();
chain.SetNext(new Manager()).SetNext(new Director());
Console.WriteLine(chain.Handle(new Purchase("A",80)));
Console.WriteLine(chain.Handle(new Purchase("B",500)));
Console.WriteLine(chain.Handle(new Purchase("C",1200)));
Console.WriteLine(chain.Handle(new Purchase("D",15000));
- Validation / processing pipelines, middleware, approval flows.
- Prefer a base handler to avoid code duplication.
How to choose the right pattern
- “I must have exactly one instance.” → Singleton
- “Creation is hairy or varies by input.” → Factory
- “Complex object assembled in steps.” → Builder
- “Cloning is cheaper than rebuilding.” → Prototype
- “Make two APIs talk.” → Adapter
- “Fan-out events to many listeners.” → Observer
- “Swap algorithms at runtime.” → Strategy
- “Add features dynamically.” → Decorator
- “Process through a pipeline.” → Chain of Responsibility
- Prefer composition (Strategy / Decorator / Adapter) over inheritance.
- Keep examples tiny; don’t introduce patterns prematurely.
- Pair patterns with unit tests to lock intent and avoid regressions.