Advanced Programming Patterns (with C# examples)

Clear explanations, when to use them, pitfalls, and compact code you can paste into a project.

Singleton

Use: one global instance Thread-safe lazy init Avoid in heavy testing

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)

Copy to clipboard
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");
When to use
  • A resource must be unique (e.g., a process-wide scheduler).
  • You need lazy initialization + thread safety.
Watch out
  • Hidden coupling & global state complicate tests.
  • Prefer DI containers for most app services.

Factory

Use: centralized creation Hide construction logic May grow into switch hell

Goal: Encapsulate object creation so callers depend on abstractions, not concrete classes.

C# (Simple Factory)

Copy to clipboard
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();
When to use
  • Construction logic is non-trivial (validation, caching).
  • You want to return different implementations by input/flags.
Watch out
  • Large switch blocks—extract to Factory Method/DI.

Builder

Use: build step-by-step Fluent APIs Overkill for simple objects

Goal: Construct complex objects step-by-step and keep construction separate from representation.

C# (Fluent Builder)

Copy to clipboard
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();
When to use
  • Many optional parts/constraints.
  • Different “recipes” for the same product.
Watch out
  • Too many builders—consider object initializer if simple.

Prototype

Use: clone to create Faster than re-building Deep vs shallow clone

Goal: Create new objects by copying an existing instance (the prototype), useful when construction is expensive.

C# (shallow clone via MemberwiseClone())

Copy to clipboard
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!
Deep clone options
  • 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

Use: make APIs compatible Wrap legacy/3rd party

Goal: Convert one interface into another clients expect, enabling classes to work together despite incompatible interfaces.

C# (object adapter)

Copy to clipboard
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());
When to use
  • Integrate legacy libraries without modifying them.
  • Present a uniform interface to multiple providers.

Observer

Use: event fan-out Publish/Subscribe Avoid memory leaks

Goal: Define a one-to-many dependency so that when the subject changes state, all observers are notified.

C# (manual observer)

Copy to clipboard
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!");
Tips
  • Unsubscribe to avoid leaks (or use weak events/Rx).
  • .NET also offers event / delegate for built-in pub/sub.

Strategy

Use: swap algorithms Runtime selection

Goal: Define a family of algorithms, encapsulate each one, and make them interchangeable.

C# (pricing strategies)

Copy to clipboard
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());
When to use
  • Multiple interchangeable algorithms (sorts, pricing, routing).
  • Avoid giant if/else trees.

Decorator

Use: add behavior at runtime Composition over inheritance Too many wrappers = complex

Goal: Attach additional responsibilities to an object dynamically by wrapping it, instead of subclassing.

C# (logging decorator)

Copy to clipboard
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");
When to use
  • Add cross-cutting concerns (logging, caching, retry) per instance.
  • Turn features on/off without subclass explosion.

Chain of Responsibility

Use: flexible pipelines Multiple handlers

Goal: Pass a request along a chain of handlers; each can handle it or pass it on.

C# (purchase approvals)

Copy to clipboard
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));
When to use
  • Validation / processing pipelines, middleware, approval flows.
Tip
  • Prefer a base handler to avoid code duplication.

How to choose the right pattern

If your problem is…
  • “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
General advice
  • 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.