Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Architecture and Design in .NET

    Design Patterns in C#: The .NET Interview Guide: back to the course

    Module 1 · Foundations: Patterns, SOLID and Judgment · Lesson 1 of 20

    Design Patterns in .NET Interviews: What They Are, the Patterns You Already Use, and How to Choose One

    Learning outcome

    You can explain what a design pattern is (and is not), name the three GoF families with one .NET example each, recognise six patterns that the .NET base class library already uses, and choose a pattern in an interview by starting from the force that hurts (what changes, what varies, what must be decoupled) instead of from a pattern name.

    Intuition

    A design pattern is a named, reusable shape for solving a recurring design problem in a context. The name is the valuable part: "put a decorator around the repository" says in five words what would otherwise take a whiteboard. Patterns are not code you copy; they are vocabulary plus a known list of trade-offs.

    In modern C#, many patterns have become language features or framework features. Events are Observer, IEnumerable<T> with yield return is Iterator, the DI container is a factory with lifetime management, and ASP.NET Core middleware is Chain of Responsibility. Interviewers for .NET roles care less about UML and more about whether you can spot the pattern in real code, say why it is there, and say when it would be overkill.

    flowchart TD
        P["What hurts?"] --> C{"Creating objects<br/>is complex or varies"}
        P --> S{"Interfaces do not fit,<br/>or behaviour must be wrapped"}
        P --> B{"Algorithms, states or<br/>communication vary"}
        C --> C1["Creational:<br/>Factory Method, Abstract Factory,<br/>Builder, Singleton, Prototype"]
        S --> S1["Structural:<br/>Adapter, Decorator, Facade,<br/>Proxy, Composite"]
        B --> B1["Behavioral:<br/>Strategy, State, Observer, Mediator,<br/>Command, Chain, Template Method, Visitor"]

    Deep dive

    • Where the catalogue comes from. Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, Vlissides, 1994, the "Gang of Four") catalogued 23 patterns in three families. This course covers the 18 that matter most in .NET interviews, plus the judgment around them.
    • The three families:
      • Creational patterns control how objects are created, so the caller does not depend on concrete classes or on a complicated construction sequence.
      • Structural patterns control how objects are composed: wrapping, adapting, simplifying or grouping objects behind a common interface.
      • Behavioral patterns control how responsibility and communication are distributed: who decides, who is notified, and which algorithm runs.
    • Every pattern has four parts. Use these when you explain one in an interview:
      1. a name;
      2. the problem and the context in which it appears;
      3. the solution shape (participants and how they collaborate);
      4. the consequences: what you gain and what you pay.
    • Patterns already in the BCL and ASP.NET Core (this lesson's code runs six of them):
      • Decorator: Stream subclasses wrap streams (GZipStream, BufferedStream, CryptoStream), and DelegatingHandler wraps HttpClient handlers.
      • Builder: StringBuilder, UriBuilder, WebApplication.CreateBuilder, IHostBuilder.
      • Strategy: IComparer<T>, IEqualityComparer<T>, StringComparer, JsonNamingPolicy.
      • Proxy: Lazy<T> (virtual proxy), EF Core lazy-loading proxies, generated gRPC clients (remote proxy), DispatchProxy.
      • Observer: C# events, IObservable<T>, IOptionsMonitor<T>.OnChange, IChangeToken.
      • Iterator: IEnumerable<T> and IAsyncEnumerable<T> with yield return.
      • Chain of Responsibility: ASP.NET Core middleware and MediatR pipeline behaviours.
      • Factory: IHttpClientFactory, ILoggerFactory, DbProviderFactory, and the DI container itself.
      • Template Method: BackgroundService.ExecuteAsync, Stream, JsonConverter<T>.
      • Visitor: ExpressionVisitor, Roslyn's CSharpSyntaxWalker.
    • How to choose, in order:
      1. Name the force. What is changing or will change? "We add a carrier every quarter" is a force; "we might need flexibility" is not.
      2. Try the plain solution first. A method, a switch expression, a delegate or a record often solves it. Lesson 18 is about this.
      3. Match force to pattern. Varying algorithm: Strategy. Behaviour depends on lifecycle state: State. Add behaviour without editing a class: Decorator. Incompatible API: Adapter. Many components talking to each other: Mediator. Cross-cutting steps around every request: Chain of Responsibility.
      4. State the cost. More types, more indirection, harder navigation in the IDE. A pattern that does not pay for its indirection is an anti-pattern.
    • What "use a pattern" means in modern .NET: usually an interface plus DI registrations, not class hierarchies. Lifetime (AddSingleton, AddScoped, AddTransient) replaces most hand-written Singletons; IEnumerable<T> injection replaces most hand-written registries; delegates replace one-method interfaces.

    Code example

    The program uses six patterns from the BCL without defining a single pattern class of its own (apart from a tiny event publisher).

    C#
    // Patterns you already use in .NET 8: the BCL is full of them.
    using System.IO.Compression;
    using System.Text;
    
    // Decorator: each Stream wraps another Stream and adds one behaviour.
    var raw = new MemoryStream();
    using (var gzip = new GZipStream(raw, CompressionLevel.SmallestSize, leaveOpen: true))
    using (var buffered = new BufferedStream(gzip))
    using (var writer = new StreamWriter(buffered))
    {
        writer.Write(new string('a', 10_000));
    }
    Console.WriteLine($"Decorator  Stream chain: 10000 chars -> {raw.Length} bytes after GZip");
    
    // Builder: StringBuilder assembles a complex value step by step, ToString() is the build step.
    var sb = new StringBuilder().Append("SELECT * FROM Orders").Append(" WHERE Total > ").Append(100);
    Console.WriteLine($"Builder    {sb}");
    
    // Strategy: the sort algorithm is fixed, the comparison is plugged in.
    string[] names = ["delta", "Alpha", "charlie", "Bravo"];
    Array.Sort(names, StringComparer.OrdinalIgnoreCase);
    Console.WriteLine($"Strategy   {string.Join(",", names)}");
    
    // Proxy (virtual proxy): Lazy<T> stands in for an expensive object until first use.
    var created = 0;
    var report = new Lazy<string>(() => { created++; return "expensive report"; });
    Console.WriteLine($"Proxy      created before Value: {created}, after: {(report.Value.Length > 0 ? created : -1)}");
    
    // Observer: events notify any number of subscribers without knowing them.
    var button = new Button();
    button.Clicked += (_, e) => Console.WriteLine($"Observer   audit handler saw click #{e}");
    button.Clicked += (_, e) => Console.WriteLine($"Observer   metrics handler saw click #{e}");
    button.Click();
    
    // Iterator: yield return builds a lazy IEnumerator for you.
    Console.WriteLine($"Iterator   {string.Join(",", Evens().Take(4))}");
    
    static IEnumerable<int> Evens() { for (var i = 0; ; i += 2) yield return i; }
    
    sealed class Button
    {
        private int _count;
        public event EventHandler<int>? Clicked;
        public void Click() => Clicked?.Invoke(this, ++_count);
    }

    Output (.NET 8, Release)

    Text
    Decorator  Stream chain: 10000 chars -> 52 bytes after GZip
    Builder    SELECT * FROM Orders WHERE Total > 100
    Strategy   Alpha,Bravo,charlie,delta
    Proxy      created before Value: 0, after: 1
    Observer   audit handler saw click #1
    Observer   metrics handler saw click #1
    Iterator   0,2,4,6

    Code walkthrough

    • Decorator: a StreamWriter writes into a BufferedStream, which writes into a GZipStream, which writes into a MemoryStream. BufferedStream and GZipStream are Stream decorators; StreamWriter is a TextWriter adapter over the byte-stream chain. 10000 repeated characters compress to 52 bytes.
    • Builder: StringBuilder.Append collects parts; ToString() (called by interpolation) produces the final string.
    • Strategy: Array.Sort is the fixed algorithm and StringComparer.OrdinalIgnoreCase is the plugged-in comparison, so Alpha,Bravo,charlie,delta sorts ignoring case.
    • Proxy: Lazy<string> stands in for the report. The factory ran 0 times before .Value and 1 time after.
    • Observer: two handlers subscribe to Clicked. The Button does not know them; both see click #1.
    • Iterator: Evens() is infinite, but Take(4) pulls only 0,2,4,6 because yield return produces values on demand.

    Real-world .NET usage

    • Reading a large compressed file is a decorator chain: FileStream → GZipStream → StreamReader.
    • Every outgoing HttpClient call made through IHttpClientFactory runs through a chain of DelegatingHandler decorators (logging, Polly resilience, authentication).
    • services.AddHttpClient<T>() is a factory; services.AddSingleton<T>() is the modern Singleton; app.UseX() builds a Chain of Responsibility.
    • EF Core's query translation walks LINQ expression trees with ExpressionVisitor.

    Trade-offs

    Benefit of using a patternCost you pay
    A shared name shortens design discussions and code reviewsReaders must know the name, or it becomes jargon
    Known extension points (add a class instead of editing a switch)More types and files; logic is spread out
    Easier testing through interfacesIndirection: "Go to definition" lands on an interface
    Proven consequences, documented pitfallsApplying a pattern "in case" adds cost with no benefit

    Failure modes

    • Starting from a pattern name ("let's use Abstract Factory") instead of from a problem.
    • Reimplementing what .NET already provides: a hand-written Singleton instead of AddSingleton, an observer list instead of an event, a pipeline instead of middleware.
    • Pattern-shaped code without the pattern's benefit: an interface with one implementation that will never vary, created only "for testability" when the class has no dependencies to fake.
    • Memorising UML diagrams without being able to say what problem the pattern solves.

    Interview angle

    When asked "Which design patterns have you used?", do not list names. Pick two and tell a short story each: the force, the pattern, the result, and the cost. Example: "Our checkout had a growing switch for shipping carriers. I moved each carrier to an IShippingStrategy, registered them in DI, and the calculator picks one by key. Adding a carrier became a new class plus a registration, and each carrier got its own unit tests. The cost was more files, which was fine because carriers changed monthly."

    Then show you know the platform: "We also use Decorator through DelegatingHandler for retries, and Chain of Responsibility through middleware."

    Practice questions

    1. What is a design pattern, in one sentence?

    Answer: A named, reusable solution shape for a recurring design problem in a given context, together with its known consequences; it is a vocabulary and a set of trade-offs, not a piece of code to copy.

    1. Name the three GoF families and give a .NET example for each.

    Answer: Creational (IHttpClientFactory, StringBuilder), structural (GZipStream as a decorator over Stream, Lazy<T> as a virtual proxy), behavioral (C# events as Observer, IComparer<T> as Strategy, middleware as Chain of Responsibility).

    1. Is a DI container a design pattern?

    Answer: It is an implementation of Inversion of Control that bundles several patterns: a factory that builds object graphs, lifetime management that replaces Singleton, and a registry. Dependency Injection itself is a technique (passing dependencies in) that makes Strategy, Decorator and others cheap to apply.

    1. How do you decide that a pattern is not worth it?

    Answer: If no force exists yet (only one variant, no expected change, no testing seam needed), the plain solution wins. Count the added types and indirection and compare them with the concrete change they make easier. Refactor to the pattern when the second or third variant actually appears.

    1. Which pattern does IEnumerable<T> with yield return implement, and what is the practical benefit?

    Answer: Iterator. The compiler generates the state machine, so callers can enumerate lazily without knowing the collection's structure, and infinite or expensive sequences produce only the items that are pulled.

    Revision checklist

    • Pattern = name + problem/context + solution shape + consequences.
    • Creational (how objects are made), structural (how they are composed), behavioral (who does what and who is told).
    • BCL examples: Stream decorators, StringBuilder, IComparer<T>, Lazy<T>, events, yield return, middleware, IHttpClientFactory.
    • Choose from the force, try the plain solution first, state the cost.

    Analogy

    Everyday picture

    Chess openings

    A chess player does not reinvent the first ten moves each game; they know named openings, what each one aims for, and its known weaknesses. They still read the board first and pick the opening that fits. Patterns are openings for code: named, studied, and only good when they fit the position.

    Quick reference

    Force → pattern

    • Object creation varies or is complex → Factory Method, Abstract Factory, Builder.
    • One shared instance → a Singleton lifetime in DI.
    • Copy a configured object → Prototype (record with).
    • Incompatible API → Adapter. Too many subsystems → Facade.
    • Add behaviour without editing → Decorator. Control access or delay creation → Proxy.
    • Tree of parts and wholes → Composite.
    • Interchangeable algorithm → Strategy. Behaviour by lifecycle state → State.
    • Fan-out notifications → Observer. Many-to-many coordination → Mediator.
    • Request as an object (queue, undo) → Command. Ordered handlers → Chain of Responsibility.
    • Fixed skeleton, varying steps → Template Method. Operations over a fixed type hierarchy → Visitor or pattern matching.
    Sign in to mark lessons done and keep your place in the course.Sign in