Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
160784 stories
·
33 followers

Copilot Code Review

1 Share

tl;dr
Copilot code review gives every pull request a fast, consistent first pass — catching typos, missing null checks and small bugs so human reviewers can focus on architecture, design and higher‑risk decisions.

What is Copilot code review?
Copilot code review is an AI‑powered reviewer that runs inside your pull‑request workflow (GitHub and integrated Azure DevOps/Azure Repos). It reads the PR diff (and respects repository custom instructions, if configured), posts inline comments as the user “GitHub Copilot,” and — where applicable — offers suggested fixes you can apply as commits directly from the PR UI. The goal is a fast, consistent first pass on correctness, style, security and maintainability so humans can spend their time on higher‑value review work.

Quick concrete example
Here’s a tiny faux PR snippet and a Copilot suggestion in GitHub’s suggestion markdown format — this demonstrates the one‑click apply behavior.

Original diff (simplified):

if (user == null) { return user.Name; }

    Copilot suggestion:

    if (user == null) { return null; }
    Or, for a safer fix it might suggest adding a null check before using Name:
    if (user == null) { throw new ArgumentNullException(nameof(user)); }

    That suggestion appears as an inline comment with an “Apply suggestion” action you can accept to create a commit.

    How it works — simple flow

    • Request a review manually or let branch/PR policies trigger one automatically.
    • Copilot ingests the PR diff and any repository custom instructions, analyzes changes, and generates a review under the “GitHub Copilot” account.
    • It posts inline comments and, where appropriate, suggestion blocks you can accept to create commits directly from the PR.
    • Reviews offer configurable effort levels (Lite/Balanced/High or Low/Medium/High). Lite = quick, style/typo fixes. High = deeper semantic and security checks; it runs longer and consumes more model usage.
    • Billing is metered via model usage; tokens are the unit used to measure consumption (bigger diffs and deeper reviews consume more tokens/credits). Monitor usage and set budgets.

    Key capabilities and innovations

    • One‑click suggested fixes that become commits — not just comments.
    • Configurable effort and scope per org/project/repo to balance cost vs thoroughness.
    • Automatic reviews via branch policies or PR events.
    • Repository‑level custom instructions so Copilot follows your team’s code style and rules.
    • Integrations for GitHub and Azure DevOps (with cost visibility in Azure Cost Management for Azure customers).
    • Enterprise features evolving toward approval assessments and configurable approvals for higher autonomy.

    Limits, constraints and operational considerations

    • Size and concurrency limits: some flows have PR file/diff limits and concurrent‑review quotas. Check the docs for current limits.
    • No automatic re‑review on new commits unless explicitly requested—plan your workflow accordingly.
    • Cost: token consumption depends on PR size, effort level and repo instructions. High default effort on high‑volume repos can increase spend.
    • Accuracy: Copilot’s suggestions are useful but not authoritative. The model can miss context or introduce subtle bugs—require human validation and CI/tests.
    • Security & privacy: Copilot needs read access to the code it reviews. Treat it like any automated tool that ingests source: apply governance, audit logs, and data‑residency settings where required.
    • Practical step: limit Copilot access to specific repos, enable repository custom instructions rather than org‑wide rules, and audit AI review activity.
    • Compliance/legal: confirm licensing/IP policies before sending proprietary code to any external AI service.

    Real‑world use cases

    • Fast first‑pass QA on every PR — reduce time‑to‑first‑feedback for junior and senior devs.
    • Enforce style and naming conventions via repository custom instructions.
    • Automatically remediate trivial issues (formatting, simple bug fixes, missing null checks).
    • Complement SAST/static analysis (CodeQL, SAST), not replace it — use Copilot to surface common anti‑patterns.
    • Improve onboarding: new hires get consistent, immediate feedback aligned with team norms.

    Pilot checklist and metrics to track
    Start small, measure impact, iterate:

    • Select 2–4 low‑risk repositories or a feature branch for a 2–4 week pilot.
    • Set conservative default effort (Lite/Balanced). Allow repo owners to escalate effort for critical PRs.
    • Enable repository custom instructions to capture team rules.
    • Run Copilot reviews in parallel with human reviews initially.

    Track these metrics:

    • Time‑to‑first‑feedback (minutes).
    • Human review hours saved per week.
    • Percent of Copilot suggestions accepted/committed.
    • False positive / incorrect suggestion rate (audit a sample).
    • Cost per PR (AI credits consumed).

    Best practices & governance

    • Use Copilot code review as a first‑pass assistant — require human approval before merging.
    • Don’t blind‑accept suggestions: run CI, tests and human checks for all applied changes.
    • Combine Copilot with your existing security tooling and audits.
    • Monitor costs and set budget alerts (use Azure Cost Management for Azure DevOps users).
    • Maintain an audit trail of AI reviews and applied suggestions for compliance.

    Programmatic integration (brief)
    To collect metrics or build dashboards, list PR review comments via the GitHub API, filter comments where the author login contains “copilot,” and parse suggestion blocks (suggestion ...). This supports dashboards for suggestions per PR, acceptance rate, and time‑to‑first‑Copilot‑comment. Don’t auto‑apply changes without governance and comprehensive tests.

    Risks to watch

    • Over‑reliance can erode human scrutiny on architecture or product intent.
    • Blind acceptance may introduce subtle or security‑sensitive bugs.
    • Cost escalation if default effort is too high across many repos.
    • Air‑gapped or highly regulated environments may require special handling or may not support all flows.

    Via Email

    An alternative to in-line code review is to receive evaluation and suggestions via email. Here is an example of a real-world PR that I received following a recent commit:

    Pull request overview
    This PR improves the robustness of the research toolchain by making Microsoft Learn MCP integration optional at startup (falling back to Tavily-only when unavailable) and by simplifying/safer logging around tool invocations in ResearcherAgent.
    
    Changes:
    
    Simplify ResearcherAgent tool-invocation logging to avoid emitting tool arguments.
    Wrap Microsoft Learn MCP client initialization/tool discovery in a startup try/catch and continue without it on failure.
    Add a dedicated startup logger for clearer warning output during initialization.
    File summaries
    File	Description
    ResearcherAgent.cs	Reduces tool invocation log detail to just the tool name (no arguments).
    Program.cs	Makes Microsoft Learn MCP tool discovery best-effort and logs a warning when unavailable.
    Review details
    Files reviewed: 2/2 changed files
    Comments generated: 2
    Review effort level: Lite
    💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
    
    In Program.cs:
    
    > +List<AIFunction> researcherTools = [tavilyTool];
    +try
    +{
    +    McpClient microsoftLearnMcp = await McpClient.CreateAsync(
    +        new HttpClientTransport(new HttpClientTransportOptions
    +        {
    +            Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
    +            Name = "microsoft-learn",
    +        }));
    +    IList<McpClientTool> microsoftLearnTools = await microsoftLearnMcp.ListToolsAsync();
    +    researcherTools.AddRange(microsoftLearnTools);
    +}
    McpClient used to be await using (IAsyncDisposable) but is now created without disposal, which can leak sockets/handlers for the lifetime of the process. Consider keeping the client alive for as long as the returned tools may need it, while still disposing it when the app exits (same as the prior pattern).
    
    In Program.cs:
    
    > +catch (Exception ex)
    +{
    +    startupLogger.LogWarning(ex, "Microsoft Learn MCP server unavailable; continuing with Tavily-only research tools.");
    +}
    The catch (Exception) will also swallow non-transient startup failures (e.g., protocol/serialization bugs), and the warning message claims “server unavailable” even if the exception is unrelated. Narrowing the catch to transient connectivity/timeouts matches the existing retry pattern above (lines 93-95) and keeps unexpected errors visible.

    Notice that the code review first restates the changes and then offers suggestions for improvement.


    References

    • GitHub docs — About Copilot code review: https://docs.github.com/copilot/concepts/agents/code-review
    • Azure DevOps — Get started with Copilot code review for pull requests: https://learn.microsoft.com/azure/devops/repos/git/copilot-code-reviews
    • GitHub Changelog — Copilot code review can now approve pull requests: https://github.blog/changelog/2026-09-01-copilot-code-review-can-now-approve-pull-requests
    • Demo video: https://www.youtube.com/watch?v=cyPaAkRfEBQ
    Read the whole story
    alvinashcraft
    just a second ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    The Design Patterns Handbook: Learn Popular Design Patterns with C# Code Examples

    1 Share

    Design patterns are reusable solutions to common problems in software design. Think of them as blueprints: not finished code, but proven templates you can adapt to solve a specific problem in your own codebase.

    This handbook serves as a practical guide to understanding software design patterns. I wrote it for every developer, regardless of the language you program in. Examples are written in C#, but every concept here applies equally to Python, Java, TypeScript, Go, and beyond.

    The source code lives at github.com/Clifftech123/design-patterns-handbook.

    Things to Keep in Mind:

    • Design patterns aren't code. They're a way of thinking about how to structure your code. They're a tool, not a silver bullet, for solving specific design problems.

    • The concepts are universal. The examples here are written in C#, but the same patterns exist in every language. If you write Python, Java, Go, or TypeScript, you're already using some of these without knowing it.

    • There's no one-size-fits-all pattern. Each pattern exists to address a particular kind of problem. Understanding what problem a pattern solves is more important than memorizing the implementation.

    I use C# here as the teaching language because it's clear, readable, and widely understood. The goal of this handbook is for you to walk away understanding the pattern itself, not just the C# code.

    There are three main types of design patterns: Creational, Structural, and Behavioral. We'll look at each one in turn here, starting with Creational.

    What We'll Cover:

    Creational Design Patterns

    Simply put, Creational patterns are all about how objects are created. They can be divided into class-creation patterns, which use inheritance to decide which class to instantiate, and object-creation patterns, which use delegation to get the job done.

    Wikipedia describes them as:

    "A creational pattern aims to separate a system from how its objects are created, composed, and represented. They increase the system's flexibility in terms of the what, who, how, and when of object creation."

    (Source)

    So Creational patterns keep the details of object creation hidden from the client code, making the system easier to manage and maintain.

    They also abstract away how objects are created, composed, and represented, so the rest of your code doesn't need to care.

    There are five Creational design patterns, which we'll go over one by one below:

    1. Singleton: Ensures a class has only one instance and provides a global point of access to it.

    2. Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate.

    3. Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

    4. Builder: Separates the construction of a complex object from its representation, so the same construction process can produce different results.

    5. Prototype: Creates new objects by cloning an existing instance rather than building one from scratch.

    1. Singleton Design Pattern

    Real World Example:

    Think of the conductor of an orchestra. An orchestra has one conductor. Every musician on stage looks to that same conductor for direction: when to start, when to stop, how fast to play, and how loud to go.

    The conductor is the single point of authority that all musicians connect to and take decisions from. You can't have two conductors standing at the front giving different instructions. That would cause chaos. No matter which musician needs guidance, they all reach the same one person.

    That's exactly how the Singleton works in code: one instance, shared by everyone who needs it, making decisions from one place.

    Problems it solves:

    • What if two musicians get different conductors giving different instructions? The performance falls apart. There must be one conductor that every musician looks to, without exception.

    • How does a musician find the conductor? They don't go searching. There's one well-known place everyone looks, and the same conductor is always there.

    • What stops someone from appointing a second conductor? The orchestra itself controls this. Once a conductor is on the podium, no second one can take it.

    In simple terms, there's only one instance of the class, and every part of the system that needs it gets access to that exact same instance (never a new one).

    Here's how Wikipedia describes the Singleton pattern:

    "In object-oriented programming, the singleton pattern is a software design pattern that restricts the instantiation of a class to a singular instance. The pattern is useful when exactly one object is needed to coordinate actions across a system." (Source)

    Programming Example:

    We'll model the analogy directly now. The OrchestraConductor is the Singleton: one instance, shared by all musicians, making all decisions.

    public class OrchestraConductor
    {
        // Step 1: Hold the one instance here
        private static OrchestraConductor _instance;
    
        // Step 2: Private constructor - nobody outside can do: new OrchestraConductor()
        private OrchestraConductor() { }
    
        // Step 3: The only way to get the conductor
        public static OrchestraConductor GetInstance()
        {
            if (_instance == null)
            {
                _instance = new OrchestraConductor();
            }
    
            return _instance;
        }
    
        // Decisions the conductor makes
        public void Start()                    => Console.WriteLine("Conductor: Begin playing.");
        public void Stop()                     => Console.WriteLine("Conductor: Stop playing.");
        public void SetTempo(string tempo)     => Console.WriteLine($"Conductor: Tempo is now {tempo}.");
    }
    

    Now let's see it in action:

    // Violinist asks for the conductor
    OrchestraConductor violinist = OrchestraConductor.GetInstance();
    
    // Pianist asks for the conductor
    OrchestraConductor pianist = OrchestraConductor.GetInstance();
    
    // Are they talking to the same conductor?
    Console.WriteLine(object.ReferenceEquals(violinist, pianist)); // True
    
    violinist.SetTempo("Allegro");
    pianist.Start();
    

    Output:

    True
    Conductor: Tempo is now Allegro.
    Conductor: Begin playing.
    

    Both musicians got the same conductor. The constructor never ran twice. That is the Singleton pattern.

    When to Use the Singleton Pattern

    Reach for Singleton when you need one shared resource that the whole application talks to, such as a logger, a configuration manager, or a database connection pool.

    It's also a good idea when having more than one instance would cause incorrect behaviour or conflicting state.

    And it's helpful when you want a global point of access to an object without passing it around everywhere.

    2. The Factory Method

    Think of a recruitment agency. A company calls the agency and says "we need a worker." The company doesn't go out and create the worker themselves. They just make the request.

    The agency decides which specific person to send: a developer, a designer, or a tester, depending on what the company needs. The company doesn't know or care exactly who is coming. They just know the person will be able to do the job.

    That's the Factory Method. Your code asks for an object. The Factory decides which specific type to create and hands it back. You work with it without needing to know exactly what it is under the hood.

    Problems it solves:

    • The company shouldn't need to know who they're getting. They just need someone who can do the job. The agency handles the decision of whom to send. The company never has to worry about the details.

    • What if the company needs a different type of worker tomorrow? They call the same agency. The agency decides. The company's process doesn't change, only the agency's decision does.

    • What if a new type of worker needs to be introduced? A new specialist agency is created to handle that. Everything else stays exactly the same.

    In simple terms, we define an interface for creating an object, but let subclasses decide which class to instantiate. The factory method lets a class defer instantiation to subclasses.

    Wikipedia describes it like this:

    "In object-oriented programming, the factory method pattern is a design pattern that uses factory methods to deal with the problem of creating objects without having to specify their exact classes. Factory methods can be specified in an interface and implemented by subclasses, or implemented in a base class and optionally overridden by subclasses." (Source)

    Programming Example:

    The agency is the factory. The worker types are the products. The company is the client.

    // The worker interface - all workers can do a job
    public interface IWorker
    {
        void DoWork();
    }
    
    // The concrete workers
    public class Developer : IWorker
    {
        public void DoWork() => Console.WriteLine("Developer: Writing code.");
    }
    
    public class Designer : IWorker
    {
        public void DoWork() => Console.WriteLine("Designer: Creating designs.");
    }
    
    // The base agency - declares the factory method
    public abstract class RecruitmentAgency
    {
        // This is the Factory Method - subclasses decide who to hire
        public abstract IWorker HireWorker();
    }
    
    // Concrete agencies - each one decides which worker to send
    public class TechAgency : RecruitmentAgency
    {
        public override IWorker HireWorker() => new Developer();
    }
    
    public class DesignAgency : RecruitmentAgency
    {
        public override IWorker HireWorker() => new Designer();
    }
    

    Now let's see it in action:

    // Company A needs a tech worker
    RecruitmentAgency agency = new TechAgency();
    IWorker worker = agency.HireWorker();
    worker.DoWork();
    
    // Company B needs a design worker
    RecruitmentAgency agency2 = new DesignAgency();
    IWorker worker2 = agency2.HireWorker();
    worker2.DoWork();
    

    Output:

    Developer: Writing code.
    Designer: Creating designs.
    

    The company never used new Developer() or new Designer() directly. The agency made that decision. That is the Factory Method.

    3. The Abstract Factory Design Pattern

    Real World Example

    Think of a furniture store that sells collections. You walk in and choose a style: Modern or Victorian. Once you choose, everything you get comes from that same collection. The sofa, the chair, and the coffee table all match. The store ensures that you never walk out with a modern sofa paired with a Victorian chair. You don't pick individual pieces and hope they go together. The collection guarantees they will.

    That's the Abstract Factory. You choose a family, and the Factory produces every object you need from that same family. Everything it gives you is guaranteed to work together.

    Problems it solves:

    • What if a customer mixes furniture from different collections? The room looks inconsistent. The store solves this by grouping everything into collections. You pick one collection and everything comes from it.

    • What if the store wants to introduce a new collection? They create a new collection set. Every existing collection stays untouched. The customer's experience doesn't change, only the options grow.

    • What if different stores carry different collections? Each store is its own factory. A customer walks into any store and follows the same process. The store handles which specific pieces to provide.

    In simple terms, you provide an interface for creating families of related objects, without specifying their concrete classes.

    Wikipedia describes it like this:

    "The abstract factory pattern provides a way to create families of related objects without imposing their concrete classes, by encapsulating a group of individual factories that have a common theme without specifying their concrete classes."

    Source: Wikipedia - Abstract factory pattern

    Programming Example:

    The furniture store is the abstract factory. Modern and Victorian are the concrete factories. Sofa and Chair are the products.

    // The product interfaces - every furniture type has a contract
    public interface ISofa  { void Describe(); }
    public interface IChair { void Describe(); }
    
    // Modern collection
    public class ModernSofa : ISofa
    {
        public void Describe() => Console.WriteLine("Sofa: Sleek modern design.");
    }
    
    public class ModernChair : IChair
    {
        public void Describe() => Console.WriteLine("Chair: Minimalist modern style.");
    }
    
    // Victorian collection
    public class VictorianSofa : ISofa
    {
        public void Describe() => Console.WriteLine("Sofa: Ornate Victorian design.");
    }
    
    public class VictorianChair : IChair
    {
        public void Describe() => Console.WriteLine("Chair: Classic Victorian style.");
    }
    
    // The abstract factory - every store can produce a sofa and a chair
    public interface IFurnitureFactory
    {
        ISofa  CreateSofa();
        IChair CreateChair();
    }
    
    // Concrete factories - each one produces its own collection
    public class ModernFurnitureFactory : IFurnitureFactory
    {
        public ISofa  CreateSofa()  => new ModernSofa();
        public IChair CreateChair() => new ModernChair();
    }
    
    public class VictorianFurnitureFactory : IFurnitureFactory
    {
        public ISofa  CreateSofa()  => new VictorianSofa();
        public IChair CreateChair() => new VictorianChair();
    }
    

    Now let's see it in action:

    // Customer orders a Modern collection
    IFurnitureFactory factory = new ModernFurnitureFactory();
    ISofa  sofa  = factory.CreateSofa();
    IChair chair = factory.CreateChair();
    sofa.Describe();
    chair.Describe();
    
    // Customer orders a Victorian collection
    IFurnitureFactory factory2 = new VictorianFurnitureFactory();
    ISofa  sofa2  = factory2.CreateSofa();
    IChair chair2 = factory2.CreateChair();
    sofa2.Describe();
    chair2.Describe();
    

    Output:

    Sofa: Sleek modern design.
    Chair: Minimalist modern style.
    Sofa: Ornate Victorian design.
    Chair: Classic Victorian style.
    

    Every piece came from the same collection. The client never used new ModernSofa() or new VictorianChair() directly. The factory kept the family together. That's the Abstract Factory.

    When to Use Abstract Factory:

    Use the Abstract Factory pattern when your system needs to work with multiple families of related objects and you need to ensure they're always used together.

    It also works well when you want to swap out an entire family of objects in one place without touching the rest of your code.

    And it's a good choice when you want to enforce consistency across related objects, so nothing from one family gets accidentally mixed with another.

    4. The Builder Design Pattern

    Real World Example

    Think of a tailor making a suit. Every customer that walks in goes through the same process: take measurements, choose the fabric, select the lining, pick the buttons, and decide on the lapel style.

    The tailor follows those same steps for every order. But the finished suit is completely unique to each customer. A businessman walks out with a sharp formal suit. A wedding guest walks out with something entirely different. Same process, same tailor, but with different result every time.

    That's the Builder. The construction process stays the same. What changes are the choices made at each step.

    Problems it solves:

    • What if a suit had to be assembled all at once with no steps? You would have to know every detail upfront and get it all right in one go. The tailor breaks it down into steps so each decision is made clearly, one at a time.

    • What if two customers want completely different suits but go through the same tailor? The tailor follows the same process for both. The steps don't change, only the choices within each step.

    • What if a new suit style needs to be introduced? A new set of choices is defined for that style. The tailoring process itself stays untouched.

    In simple terms, you separate the construction of a complex object from its representation, so that the same construction process can create different results.

    Wikipedia describes it like this:

    "The Builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations." (Source)

    Programming Example:

    The tailor is the director. The suit is the product. The builder handles the step-by-step construction.

    // The product
    public class Suit
    {
        public string Fabric  { get; set; }
        public string Lining  { get; set; }
        public string Buttons { get; set; }
    
        public void Describe()
        {
            Console.WriteLine($"Suit: {Fabric} fabric, {Lining} lining, {Buttons} buttons.");
        }
    }
    
    // The builder - defines the steps
    public interface ISuitBuilder
    {
        void SetFabric();
        void SetLining();
        void SetButtons();
        Suit GetSuit();
    }
    
    // Business suit builder
    public class BusinessSuitBuilder : ISuitBuilder
    {
        private Suit _suit = new Suit();
    
        public void SetFabric()  => _suit.Fabric  = "Dark wool";
        public void SetLining()  => _suit.Lining  = "Silk";
        public void SetButtons() => _suit.Buttons = "Black horn";
        public Suit GetSuit()    => _suit;
    }
    
    // Wedding suit builder
    public class WeddingSuitBuilder : ISuitBuilder
    {
        private Suit _suit = new Suit();
    
        public void SetFabric()  => _suit.Fabric  = "Ivory linen";
        public void SetLining()  => _suit.Lining  = "Satin";
        public void SetButtons() => _suit.Buttons = "Pearl";
        public Suit GetSuit()    => _suit;
    }
    
    // The tailor - the director who runs the process
    public class Tailor
    {
        public Suit MakeSuit(ISuitBuilder builder)
        {
            builder.SetFabric();
            builder.SetLining();
            builder.SetButtons();
            return builder.GetSuit();
        }
    }
    

    Now let's see it in action:

    Tailor tailor = new Tailor();
    
    Suit businessSuit = tailor.MakeSuit(new BusinessSuitBuilder());
    businessSuit.Describe();
    
    Suit weddingSuit = tailor.MakeSuit(new WeddingSuitBuilder());
    weddingSuit.Describe();
    

    Output:

    Suit: Dark wool fabric, Silk lining, Black horn buttons.
    Suit: Ivory linen fabric, Satin lining, Pearl buttons.
    

    The same tailor, following the same process, creates two completely different suits. That is the Builder.

    When to Use the Builder Design Pattern

    Use the Builder pattern when an object has many parts or configurations and building it all at once would be confusing.

    It's also a good choice when you want the same construction process to produce different results depending on the choices made at each step.

    And reach for it when you want to keep the construction logic separate from the object itself, so each can change independently.

    5. The Prototype Design Pattern

    Real World Example

    Imagine you're building a drawing application. Users can create shapes like circles, rectangles, or triangles, each with its own colour, size, and position.

    Now imagine the user wants ten red circles of the same size placed across the canvas. Creating each one from scratch means repeating the same setup ten times. What if the shape is complex, with many configured properties? That becomes expensive and repetitive.

    The Prototype pattern solves this by letting you take one fully configured shape and clone it. The clone starts as an exact copy. The user then moves it, recolours it, or resizes it independently. The original shape is never touched. This also means new shape types can be added at runtime without the application needing to know about them in advance.

    Problems it solves:

    • Creating a new shape from scratch every time is expensive. If a shape has many properties, setting them all up repeatedly wastes resources. Cloning an already configured object is far cheaper.

    • The application shouldn't need to know the exact type of shape it's copying. At runtime, shapes can be added or removed dynamically. The app just calls clone and gets back a ready object, whatever type it happens to be.

    • Modifying a copy should never affect the original. Each cloned shape is fully independent. Changes to the copy stay with the copy.

    In simple terms, you can create new objects by copying an existing one. The copy starts identical to the original and can then be changed independently.

    Wikipedia describes it like this:

    "The Prototype pattern is used when the type of objects to create is determined by a prototypical instance, which is cloned to produce new objects." (Source)

    Programming Example:

    Every shape knows how to clone itself. The application never calls new Circle() or new Rectangle() directly at runtime. It clones what already exists.

    // The prototype interface - every shape must be able to clone itself
    public abstract class Shape
    {
        public string Colour { get; set; }
        public int    Size   { get; set; }
    
        public abstract Shape Clone();
        public abstract void  Describe();
    }
    
    // Concrete shapes
    public class Circle : Shape
    {
        public override Shape Clone()    => (Shape)this.MemberwiseClone();
        public override void  Describe() => Console.WriteLine($"Circle  | Colour: {Colour} | Size: {Size}");
    }
    
    public class Rectangle : Shape
    {
        public override Shape Clone()    => (Shape)this.MemberwiseClone();
        public override void  Describe() => Console.WriteLine($"Rectangle | Colour: {Colour} | Size: {Size}");
    }
    

    Now let's see it in action:

    // Create one configured circle
    Circle original = new Circle { Colour = "Red", Size = 50 };
    
    // Clone it instead of building from scratch
    Shape clone1 = original.Clone();
    Shape clone2 = original.Clone();
    
    // Modify the clones independently
    clone2.Colour = "Blue";
    
    original.Describe();
    clone1.Describe();
    clone2.Describe();
    

    Output:

    Circle  | Colour: Red  | Size: 50
    Circle  | Colour: Red  | Size: 50
    Circle  | Colour: Blue | Size: 50
    

    clone2 changed to blue. The original stayed red. Each object is fully independent. That's the Prototype.

    When to Use the Prototype Design Pattern:

    Use Prototype when creating a new object from scratch is expensive or complex and an existing object already has everything configured.

    It's also helpful when the application needs to create objects at runtime without knowing their exact type in advance.

    And it's great when you need many variations of an object and want to start from a known good state rather than rebuild every time.

    Structural Design Patterns

    Simply put, structural patterns are all about how classes and objects are composed to form larger structures. They use inheritance and composition to let you build flexible, efficient structures without having to rewrite everything from scratch.

    Wikipedia describes them as:

    "In software engineering, structural patterns are design patterns that ease the design by identifying a simple way to realize relationships among entities." (Source)

    Structural design patterns describe how objects and classes are combined to form larger, more complex structures while keeping those structures flexible and efficient.

    They focus on composition over inheritance: how you connect things, not just what things are.

    There are seven Structural design patterns:

    1. Adapter: Converts one interface into another that a client expects, letting incompatible interfaces work together.

    2. Bridge: Decouples an abstraction from its implementation so the two can vary independently.

    3. Composite: Composes objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly.

    4. Decorator: Attaches additional responsibilities to an object dynamically, as a flexible alternative to subclassing.

    5. Facade: Provides a simplified, unified interface to a complex subsystem.

    6. Flyweight: Uses sharing to efficiently support a large number of fine-grained objects.

    7. Proxy: Provides a surrogate or placeholder for another object to control access to it.

    1. The Adapter Design Pattern

    Real World Example

    Think of a language translator at a business meeting. A British CEO needs to address a Japanese team. The CEO speaks only English. The team speaks only Japanese. A translator sits between them, converting every English sentence into Japanese and delivering it to the team. Both sides keep speaking their own language. Neither the CEO nor the team change anything about how they communicate. The translator makes them compatible.

    That's the Adapter. The client speaks one interface, while the other side speaks a different one. The Adapter sits between them and makes both sides work together without either having to change.

    Problems it solves:

    • The CEO can't speak Japanese, and the team can't speak English. They're incompatible. The translator adapts one to the other without changing either side.

    • What if the CEO now needs to address a French team? A French translator is brought in. The CEO's process doesn't change. Only the translator changes.

    • What if an existing class has a useful method but the wrong interface? You wrap it in an adapter. The rest of the system talks to the adapter while the existing class stays untouched.

    In simple terms, you wrap an existing class with a new interface so the client can use it without any changes to either side.

    Wikipedia describes it like this:

    "In software engineering, the adapter pattern is a software design pattern (also known as wrapper) that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code." (Source)

    Programming Example:

    The CEO is the client and the Japanese team member is the adaptee. They're useful, but they speak the wrong interface. The Translator is the adapter.

    // What the CEO expects, someone who can receive a message in English
    public interface IEnglishSpeaker
    {
        void Speak(string message);
    }
    
    // The Japanese team member, speaks only Japanese (the adaptee)
    public class JapaneseTeamMember
    {
        public void SpeakJapanese(string message)
        {
            Console.WriteLine($"Team member (Japanese): {message}");
        }
    }
    
    // The Translator, adapts the Japanese speaker to the English interface
    public class Translator : IEnglishSpeaker
    {
        private readonly JapaneseTeamMember _teamMember;
    
        public Translator(JapaneseTeamMember teamMember)
        {
            _teamMember = teamMember;
        }
    
        public void Speak(string message)
        {
            string translated = TranslateToJapanese(message);
            _teamMember.SpeakJapanese(translated);
        }
    
        private string TranslateToJapanese(string english) => english switch
        {
            "Good morning, team."         => "おはようございます、チームの皆さん。",
            "Please review the proposal." => "提案書を確認してください。",
            _                             => $"[Japanese: {english}]"
        };
    }
    
    // The CEO, only knows how to talk to an IEnglishSpeaker
    public class CEO
    {
        private readonly IEnglishSpeaker _speaker;
    
        public CEO(IEnglishSpeaker speaker)
        {
            _speaker = speaker;
        }
    
        public void Address(string message)
        {
            Console.WriteLine($"CEO (English): {message}");
            _speaker.Speak(message);
        }
    }
    

    Now let's see it in action:

    JapaneseTeamMember teamMember = new JapaneseTeamMember();
    IEnglishSpeaker translator    = new Translator(teamMember);
    CEO ceo = new CEO(translator);
    
    ceo.Address("Good morning, team.");
    ceo.Address("Please review the proposal.");
    

    Output:

    CEO (English): Good morning, team.
    Team member (Japanese): おはようございます、チームの皆さん。
    CEO (English): Please review the proposal.
    Team member (Japanese): 提案書を確認してください。
    

    The CEO never knew about JapaneseTeamMember. The team never knew about the CEO's interface. The Translator made both sides work together without touching either. That's the Adapter.

    When to Use it

    Use Adapter when you want to use an existing class but its interface doesn't match what your code expects.

    It's also helpful when you want to create a reusable class that cooperates with classes that don't have compatible interfaces.

    And reach for it when you need to integrate a third-party library or legacy code without modifying it.

    2. The Bridge Design Pattern

    Real World Example

    Think of a TV remote control and a television. The remote is one thing and the TV is another. You can have a basic remote or a smart remote. You can have a Sony TV or a Samsung TV. Any remote works with any TV you're not locked in. Buy a new Samsung TV, and your old remote still works. Buy a smart universal remote, and it works with every TV you own. Neither side needs to know the inner details of the other.

    That's the Bridge. The abstraction (remote) and the implementation (TV) are two separate hierarchies that can grow and change completely independently of each other.

    Problems it solves:

    • What if every remote was hardwired to one specific TV brand? You would need a SonyBasicRemote, a SamsungBasicRemote, a SonySmartRemote, a SamsungSmartRemote...and so on. One class for every combination. Adding one new TV brand would double your remote classes. The Bridge stops this explosion.

    • What if you want to add a new remote type without touching the TVs? With Bridge, you just create a new remote class. The TVs are untouched.

    • What if you want to add a new TV brand without touching the remotes? Same answer. You add a new TV class. Every existing remote already works with it.

    In simple terms, you split a large class into two separate hierarchies (the abstraction and the implementation) so each can be changed and extended without affecting the other.

    Wikipedia describes it like this:

    "The bridge pattern is a design pattern used in software engineering that is meant to decouple an abstraction from its implementation so that the two can vary independently." (Source)

    Programming Example:

    The remote control is the abstraction and the TV brand is the implementation. They're connected through a bridge (the ITV interface), but neither hierarchy depends on the other's details.

    // The implementation interface — what any TV must be able to do
    public interface ITV
    {
        void TurnOn();
        void TurnOff();
        void SetChannel(int channel);
        void SetVolume(int volume);
    }
    
    // Concrete implementations — each brand handles things its own way
    public class SonyTV : ITV
    {
        public void TurnOn()           => Console.WriteLine("Sony TV: Powering on. BRAVIA display ready.");
        public void TurnOff()          => Console.WriteLine("Sony TV: Shutting down.");
        public void SetChannel(int ch) => Console.WriteLine($"Sony TV: Switching to channel {ch}.");
        public void SetVolume(int vol) => Console.WriteLine($"Sony TV: Volume set to {vol}.");
    }
    
    public class SamsungTV : ITV
    {
        public void TurnOn()           => Console.WriteLine("Samsung TV: Turning on. Smart Hub loading.");
        public void TurnOff()          => Console.WriteLine("Samsung TV: Powering off.");
        public void SetChannel(int ch) => Console.WriteLine($"Samsung TV: Channel {ch} selected.");
        public void SetVolume(int vol) => Console.WriteLine($"Samsung TV: Volume at {vol}.");
    }
    
    // The abstraction — the remote holds a reference to whichever TV it controls
    public abstract class RemoteControl
    {
        protected ITV _tv;
    
        protected RemoteControl(ITV tv) { _tv = tv; }
    
        public abstract void TurnOn();
        public abstract void TurnOff();
        public abstract void SetChannel(int channel);
    }
    
    // Refined abstraction — a basic remote, does exactly what the TV does
    public class BasicRemote : RemoteControl
    {
        public BasicRemote(ITV tv) : base(tv) { }
    
        public override void TurnOn()           => _tv.TurnOn();
        public override void TurnOff()          => _tv.TurnOff();
        public override void SetChannel(int ch) => _tv.SetChannel(ch);
    }
    
    // Refined abstraction — a smart remote, adds its own behaviour on top
    public class SmartRemote : RemoteControl
    {
        public SmartRemote(ITV tv) : base(tv) { }
    
        public override void TurnOn()
        {
            Console.WriteLine("Smart Remote: Activating voice control.");
            _tv.TurnOn();
        }
    
        public override void TurnOff()
        {
            Console.WriteLine("Smart Remote: Saving watch history.");
            _tv.TurnOff();
        }
    
        public override void SetChannel(int ch)
        {
            Console.WriteLine("Smart Remote: Looking up channel guide.");
            _tv.SetChannel(ch);
        }
    
        public void SetVolume(int vol) => _tv.SetVolume(vol);
    }
    

    Now let's see it in action:

    // Basic remote paired with a Sony TV
    Console.WriteLine("--- Basic Remote + Sony TV ---");
    RemoteControl basicSony = new BasicRemote(new SonyTV());
    basicSony.TurnOn();
    basicSony.SetChannel(5);
    basicSony.TurnOff();
    
    // Smart remote paired with a Samsung TV
    Console.WriteLine("\n--- Smart Remote + Samsung TV ---");
    SmartRemote smartSamsung = new SmartRemote(new SamsungTV());
    smartSamsung.TurnOn();
    smartSamsung.SetChannel(10);
    smartSamsung.SetVolume(20);
    smartSamsung.TurnOff();
    
    // Swap freely — smart remote now with Sony, no code changes needed
    Console.WriteLine("\n--- Smart Remote + Sony TV ---");
    SmartRemote smartSony = new SmartRemote(new SonyTV());
    smartSony.TurnOn();
    smartSony.SetChannel(3);
    smartSony.TurnOff();
    

    Output:

    --- Basic Remote + Sony TV ---
    Sony TV: Powering on. BRAVIA display ready.
    Sony TV: Switching to channel 5.
    Sony TV: Shutting down.
    
    --- Smart Remote + Samsung TV ---
    Smart Remote: Activating voice control.
    Samsung TV: Turning on. Smart Hub loading.
    Smart Remote: Looking up channel guide.
    Samsung TV: Channel 10 selected.
    Samsung TV: Volume at 20.
    Smart Remote: Saving watch history.
    Samsung TV: Powering off.
    
    --- Smart Remote + Sony TV ---
    Smart Remote: Activating voice control.
    Sony TV: Powering on. BRAVIA display ready.
    Smart Remote: Looking up channel guide.
    Sony TV: Switching to channel 3.
    Smart Remote: Saving watch history.
    Sony TV: Shutting down.
    

    The same SmartRemote worked with both Sony and Samsung without any changes. Adding a new TV brand like LG means creating one new class, and every existing remote works with it immediately. That's the Bridge.

    When to Use it

    Use the Bridge pattern when you want to avoid a permanent binding between an abstraction and its implementation, so either can be swapped at runtime.

    You can also use it when both the abstraction and the implementation should be independently extensible through subclassing.

    And it's a good fit when changes to the implementation should have no impact on the client code. The client shouldn't need to be recompiled.

    3. The Composite Design Pattern

    Real World Example

    Think of a company organisation chart. A company has a CEO. Under the CEO are department heads, each leading a department full of employees. Under some departments are even smaller sub-teams.

    Now imagine you want to know the total salary cost. You can ask a single employee they tell you their salary. You can ask a whole department, which adds up every person inside it, including nested teams. Or you can ask the entire company: it rolls up every salary across every level.

    The same question, asked the same way, whether you're talking to one person or thousands.

    That's the Composite pattern. Individual items and groups of items share the same interface. The caller never needs to know which one they're dealing with.

    Problems it solves:

    • What if you had to write different code to handle a single employee versus a whole department? You would end up with if checks everywhere just to figure out what you're talking to. Composite removes that entirely: one interface, always.

    • What if departments can contain other departments? Composite handles any depth of nesting naturally. The caller just asks the top of the tree and the operation flows down automatically.

    • What if you want to add a new type of team or role? You implement the same interface. Everything above it in the tree keeps working without any changes.

    In simple terms, you compose objects into tree structures. This lets individual objects and groups of objects be treated through the same interface, so the caller never has to care about the difference.

    Wikipedia describes it like this:

    "The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object. The intent of a composite is to compose objects into tree structures to represent part-whole hierarchies." (Source)

    Programming Example:

    Every node in the tree (whether a single employee or an entire department) implements IEmployee. The caller treats them identically.

    // The component interface — every leaf and composite shares this contract
    public interface IEmployee
    {
        string Name    { get; }
        int    GetSalary();
        void   GetDetails(string indent = "");
    }
    
    // The leaf — a single employee with no reports
    public class Employee : IEmployee
    {
        private readonly int _salary;
    
        public string Name { get; }
    
        public Employee(string name, int salary)
        {
            Name    = name;
            _salary = salary;
        }
    
        public int  GetSalary()                    => _salary;
        public void GetDetails(string indent = "") => Console.WriteLine($"{indent}- {Name} (£{_salary:N0})");
    }
    
    // The composite — a department that holds employees or other departments
    public class Department : IEmployee
    {
        private readonly List<IEmployee> _members = new();
    
        public string Name { get; }
    
        public Department(string name) { Name = name; }
    
        public void Add(IEmployee employee)    => _members.Add(employee);
        public void Remove(IEmployee employee) => _members.Remove(employee);
    
        public int GetSalary() => _members.Sum(m => m.GetSalary());
    
        public void GetDetails(string indent = "")
        {
            Console.WriteLine($"{indent}[{Name}] Total: £{GetSalary():N0}");
            foreach (var member in _members)
                member.GetDetails(indent + "  ");
        }
    }
    

    Now let's see it in action:

    // Individual employees
    var ceo        = new Employee("Alice (CEO)",        120_000);
    var cto        = new Employee("Bob (CTO)",           95_000);
    var dev1       = new Employee("Carol (Developer)",   65_000);
    var dev2       = new Employee("David (Developer)",   62_000);
    var cfo        = new Employee("Eve (CFO)",           90_000);
    var accountant = new Employee("Frank (Accountant)",  55_000);
    
    // Build the Engineering department
    var engineering = new Department("Engineering");
    engineering.Add(cto);
    engineering.Add(dev1);
    engineering.Add(dev2);
    
    // Build the Finance department
    var finance = new Department("Finance");
    finance.Add(cfo);
    finance.Add(accountant);
    
    // Build the whole company
    var company = new Department("Acme Corp");
    company.Add(ceo);
    company.Add(engineering);
    company.Add(finance);
    
    // Ask the whole company — one call, rolls up everything
    Console.WriteLine("=== Full Company ===");
    company.GetDetails();
    
    // Ask just one department — same call, same interface
    Console.WriteLine("\n=== Engineering Only ===");
    engineering.GetDetails();
    
    // Ask a single employee — same call, same interface
    Console.WriteLine("\n=== Single Employee ===");
    dev1.GetDetails();
    

    Output:

    === Full Company ===
    [Acme Corp] Total: £487,000
      - Alice (CEO) (£120,000)
      [Engineering] Total: £222,000
        - Bob (CTO) (£95,000)
        - Carol (Developer) (£65,000)
        - David (Developer) (£62,000)
      [Finance] Total: £145,000
        - Eve (CFO) (£90,000)
        - Frank (Accountant) (£55,000)
    
    === Engineering Only ===
    [Engineering] Total: £222,000
      - Bob (CTO) (£95,000)
      - Carol (Developer) (£65,000)
      - David (Developer) (£62,000)
    
    === Single Employee ===
    - Carol (Developer) (£65,000)
    

    company.GetDetails(), engineering.GetDetails(), and dev1.GetDetails(): the same call on three different levels of the tree. The caller never checked what it was talking to. That's the Composite pattern.

    When to Use it

    Use Composite when you need to represent part-whole hierarchies, like trees where individual items and groups of items need to be used interchangeably.

    It also works well when you want client code to treat single objects and collections of objects uniformly, without any special-casing.

    And it's a good fit when the structure can be nested to any depth and that depth shouldn't affect how the caller interacts with it.

    4. The Decorator Design Pattern

    Real World Example

    Think of ordering a coffee at a cafe. You start with a plain espresso. Then you ask for milk. Then vanilla syrup. Then whipped cream on top. Each addition wraps around or adds to what was already there, adding its own cost and its own description. The espresso at the centre never changes. You're just layering on top of it, one addition at a time. You could add two shots of syrup. You could skip the milk entirely. Every combination is possible without creating a new type of coffee for each one.

    That's the Decorator pattern. You start with a base object and wrap it in layers. Each layer adds its own behaviour and then delegates to whatever is underneath it.

    Problems it solves:

    • What if you needed a class for every combination? EspressoWithMilk, EspressoWithMilkAndVanilla, EspressoWithMilkAndVanillaAndCream...the list explodes. Decorator adds behaviour at runtime, so you never need those classes.

    • What if the base coffee shouldn't change? It does not. The espresso class stays untouched. The decorators wrap around it and extend it independently.

    • What if a new topping needs to be added? You create one new decorator class. Every existing combination still works exactly as before.

    In simple terms, you wrap an object in one or more layers, where each layer adds its own behaviour before or after delegating to the layer beneath it.

    Wikipedia describes it like this:

    "The decorator pattern is a design pattern that allows behaviour to be added to an individual object, dynamically, without affecting the behaviour of other instances of the same class." (Source)

    Programming Example:

    The coffee is the component. Each topping is a decorator. Every decorator wraps the component and adds to its description and cost.

    // The component interface — every coffee, plain or decorated, shares this
    public interface ICoffee
    {
        string GetDescription();
        double GetCost();
    }
    
    // The base component — a plain espresso
    public class Espresso : ICoffee
    {
        public string GetDescription() => "Espresso";
        public double GetCost()        => 1.00;
    }
    
    // The base decorator — wraps any ICoffee and delegates to it
    public abstract class CoffeeDecorator : ICoffee
    {
        protected readonly ICoffee _coffee;
    
        protected CoffeeDecorator(ICoffee coffee) { _coffee = coffee; }
    
        public virtual string GetDescription() => _coffee.GetDescription();
        public virtual double GetCost()        => _coffee.GetCost();
    }
    
    // Concrete decorators — each one adds its own layer
    public class Milk : CoffeeDecorator
    {
        public Milk(ICoffee coffee) : base(coffee) { }
    
        public override string GetDescription() => _coffee.GetDescription() + ", Milk";
        public override double GetCost()        => _coffee.GetCost() + 0.30;
    }
    
    public class VanillaSyrup : CoffeeDecorator
    {
        public VanillaSyrup(ICoffee coffee) : base(coffee) { }
    
        public override string GetDescription() => _coffee.GetDescription() + ", Vanilla Syrup";
        public override double GetCost()        => _coffee.GetCost() + 0.50;
    }
    
    public class WhippedCream : CoffeeDecorator
    {
        public WhippedCream(ICoffee coffee) : base(coffee) { }
    
        public override string GetDescription() => _coffee.GetDescription() + ", Whipped Cream";
        public override double GetCost()        => _coffee.GetCost() + 0.75;
    }
    

    Now let's see it in action:

    // A plain espresso
    ICoffee order = new Espresso();
    Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
    
    // Wrap it with milk
    order = new Milk(order);
    Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
    
    // Wrap it with vanilla syrup on top
    order = new VanillaSyrup(order);
    Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
    
    // Wrap it with whipped cream on top of that
    order = new WhippedCream(order);
    Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
    

    Output:

    Espresso => £1.00
    Espresso, Milk => £1.30
    Espresso, Milk, Vanilla Syrup => £1.80
    Espresso, Milk, Vanilla Syrup, Whipped Cream => £2.55
    

    Each line is a new layer wrapped around the previous one. The espresso never changed. The cost and description grew with every wrapper. That's the Decorator pattern.

    When to Use it

    Use the Decorator pattern when you want to add responsibilities to individual objects without affecting other objects of the same class.

    It's also a good choice when subclassing would lead to an explosion of classes to cover every possible combination of behaviours.

    And use it when you need to be able to stack behaviours in any order at runtime, independently of each other.

    5. The Facade Design Pattern

    Real World Example

    Think of clicking "Place Order" on a shopping website. In that single click, several things happen behind the scenes: the system checks whether the item is in stock, your payment is charged, a shipping label is generated, and a confirmation email is sent to you. You don't see any of that. You click one button and get one result. The complexity of four separate systems is hidden behind a single, clean action.

    That's the Facade pattern: one simple interface in front of many complex moving parts. The caller doesn't need to know what's happening behind the scenes.

    Problems it solves:

    • What if the client had to call each subsystem directly? Check inventory, then process payment, then generate a label, and then send an email, all in the right order, handling each failure separately. The Facade wraps all of that into one call.

    • What if one of the subsystems changes? The Facade absorbs the change. The client code never needs to know. Only the Facade is updated.

    • What if different clients need the same flow? They all call the same Facade method. The logic is in one place, not duplicated across every caller.

    In simple terms, you provide a single, simple interface that hides the complexity of a set of subsystems behind it.

    Wikipedia describes it like this:

    "The facade pattern (also spelled façade) is a software-design pattern commonly used in object-oriented programming. Analogous to a facade in architecture, a facade is an object that serves as a front-facing interface masking more complex underlying or structural code."(Source)

    Programming Example:

    Each subsystem does its own job. The OrderFacade is the single entry point that coordinates all of them. The client only ever talks to the facade.

    // Subsystem 1: checks whether the item is available
    public class InventoryService
    {
        public bool CheckStock(string item)
        {
            Console.WriteLine($"Inventory: Checking stock for {item}.");
            return true;
        }
    }
    
    // Subsystem 2: handles the payment
    public class PaymentService
    {
        public bool ProcessPayment(string cardNumber, double amount)
        {
            Console.WriteLine($"Payment: Charging £{amount:F2} to card ending {cardNumber[^4..]}.");
            return true;
        }
    }
    
    // Subsystem 3: generates a shipping label
    public class ShippingService
    {
        public string GenerateLabel(string item, string address)
        {
            Console.WriteLine($"Shipping: Generating label for {item} to {address}.");
            return "TRACK-29384";
        }
    }
    
    // Subsystem 4: sends the confirmation email
    public class EmailService
    {
        public void SendConfirmation(string email, string trackingCode)
        {
            Console.WriteLine($"Email: Confirmation sent to {email}. Tracking code: {trackingCode}.");
        }
    }
    
    // The Facade — one method, hides all four subsystems
    public class OrderFacade
    {
        private readonly InventoryService _inventory = new();
        private readonly PaymentService   _payment   = new();
        private readonly ShippingService  _shipping  = new();
        private readonly EmailService     _email     = new();
    
        public void PlaceOrder(string item, string cardNumber, double amount, string address, string email)
        {
            Console.WriteLine("=== Placing Order ===");
    
            if (!_inventory.CheckStock(item))
            {
                Console.WriteLine("Order failed: item out of stock.");
                return;
            }
    
            if (!_payment.ProcessPayment(cardNumber, amount))
            {
                Console.WriteLine("Order failed: payment declined.");
                return;
            }
    
            string trackingCode = _shipping.GenerateLabel(item, address);
            _email.SendConfirmation(email, trackingCode);
    
            Console.WriteLine($"\nOrder complete. Your tracking code is {trackingCode}.");
        }
    }
    

    Now let's see it in action:

    OrderFacade store = new OrderFacade();
    
    store.PlaceOrder(
        item:       "Wireless Headphones",
        cardNumber: "4111111111111234",
        amount:     79.99,
        address:    "42 Maple Street, London",
        email:      "customer@email.com"
    );
    

    Output:

    === Placing Order ===
    Inventory: Checking stock for Wireless Headphones.
    Payment: Charging £79.99 to card ending 1234.
    Shipping: Generating label for Wireless Headphones to 42 Maple Street, London.
    Email: Confirmation sent to customer@email.com. Tracking code: TRACK-29384.
    
    Order complete. Your tracking code is TRACK-29384.
    

    The client called one method. Four subsystems ran in the right order. None of that complexity was visible to the caller. That is the Facade.

    When to Use it

    Use Facade when you want to provide a simple interface to a complex subsystem so callers aren't burdened by its internals.

    It also works well when you want to layer your system so that high-level code talks to facades, not directly to low-level subsystems.

    And it's a good choice when you want a single entry point that coordinates a sequence of steps across multiple services.

    6. The Flyweight Design Pattern

    Real World Example

    Think of a game that renders a forest. The forest has ten thousand trees. Each tree has a type name, a colour, and a texture. But most of those trees are Oaks, and all Oaks look exactly the same.

    Creating ten thousand separate objects, each storing the same name, colour, and texture, wastes enormous amounts of memory. Instead, you create one shared Oak object that holds all that data. Every Oak tree in the forest points to that same shared object and only stores its own position on the map.

    That's the Flyweight pattern. The data that's the same across many instances is shared. The data that's unique per instance is stored separately and passed in only when needed.

    Problems it solves:

    • What if you created a full object for every single tree? With ten thousand trees, you store the same name, colour, and texture ten thousand times. Flyweight stores that shared data once and reuses it everywhere.

    • What if a new tree type is introduced? The factory creates one new shared object for it. Every tree of that type immediately uses it without any extra memory.

    • What if the forest needs to render each tree at its own position? The position is unique per tree, so it's stored on the tree itself and passed to the shared object only at render time. The shared object never holds it.

    In simple terms, you split an object's data into what's shared across many instances and what's unique per instance. Share the common part. Pass the unique part in only when needed.

    Wikipedia describes it like this:

    "A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects. It is a way to use objects in large numbers when a simple repeated representation would use an unacceptable amount of memory." (Source)

    Programming ExampleL

    TreeType is the flyweight: it holds shared data. Tree holds only the unique position and a reference to a shared TreeType. The factory ensures each TreeType is created only once.

    // The flyweight — holds shared intrinsic state (same for all trees of this type)
    public class TreeType
    {
        public string Name    { get; }
        public string Colour  { get; }
        public string Texture { get; }
    
        public TreeType(string name, string colour, string texture)
        {
            Name    = name;
            Colour  = colour;
            Texture = texture;
        }
    
        public void Render(int x, int y)
        {
            Console.WriteLine($"Rendering {Name} tree ({Colour}, {Texture}) at ({x}, {y})");
        }
    }
    
    // The flyweight factory — creates and caches tree types so they are never duplicated
    public class TreeTypeFactory
    {
        private readonly Dictionary<string, TreeType> _cache = new();
    
        public TreeType GetTreeType(string name, string colour, string texture)
        {
            string key = $"{name}_{colour}_{texture}";
    
            if (!_cache.ContainsKey(key))
            {
                Console.WriteLine($"Factory: Creating new TreeType for '{name}'.");
                _cache[key] = new TreeType(name, colour, texture);
            }
    
            return _cache[key];
        }
    
        public int TotalTypes => _cache.Count;
    }
    
    // The context — holds unique extrinsic state (position) and a reference to a shared flyweight
    public class Tree
    {
        private readonly int      _x;
        private readonly int      _y;
        private readonly TreeType _type;
    
        public Tree(int x, int y, TreeType type)
        {
            _x    = x;
            _y    = y;
            _type = type;
        }
    
        public void Render() => _type.Render(_x, _y);
    }
    
    // The forest — plants trees using shared flyweights
    public class Forest
    {
        private readonly List<Tree>      _trees   = new();
        private readonly TreeTypeFactory _factory = new();
    
        public void PlantTree(int x, int y, string name, string colour, string texture)
        {
            TreeType type = _factory.GetTreeType(name, colour, texture);
            _trees.Add(new Tree(x, y, type));
        }
    
        public void Render()
        {
            foreach (var tree in _trees)
                tree.Render();
        }
    
        public int TreeCount     => _trees.Count;
        public int TreeTypeCount => _factory.TotalTypes;
    }
    

    Now let's see it in action:

    Forest forest = new Forest();
    
    // Plant 6 trees — but only 2 unique types
    forest.PlantTree(1,  5,  "Oak",  "Dark Green",  "Rough bark");
    forest.PlantTree(3,  12, "Oak",  "Dark Green",  "Rough bark");
    forest.PlantTree(7,  2,  "Oak",  "Dark Green",  "Rough bark");
    forest.PlantTree(10, 8,  "Pine", "Light Green", "Smooth bark");
    forest.PlantTree(15, 3,  "Pine", "Light Green", "Smooth bark");
    forest.PlantTree(20, 14, "Pine", "Light Green", "Smooth bark");
    
    forest.Render();
    
    Console.WriteLine($"\nTrees planted:              {forest.TreeCount}");
    Console.WriteLine($"Unique tree types in memory: {forest.TreeTypeCount}");
    

    Output:

    Factory: Creating new TreeType for 'Oak'.
    Factory: Creating new TreeType for 'Pine'.
    Rendering Oak tree (Dark Green, Rough bark) at (1, 5)
    Rendering Oak tree (Dark Green, Rough bark) at (3, 12)
    Rendering Oak tree (Dark Green, Rough bark) at (7, 2)
    Rendering Pine tree (Light Green, Smooth bark) at (10, 8)
    Rendering Pine tree (Light Green, Smooth bark) at (15, 3)
    Rendering Pine tree (Light Green, Smooth bark) at (20, 14)
    
    Trees planted:              6
    Unique tree types in memory: 2
    

    Six trees, but only two TreeType objects were ever created. Scale that to ten thousand trees and the factory still creates exactly two. The positions are unique per tree, and the appearance is shared. That's the Flyweight.

    When to Use it

    Use Flyweight when your application needs to create a very large number of similar objects that would otherwise consume too much memory.

    It's also useful when most of the object's state can be made shared across instances, with only a small part being unique per instance.

    And it's a good choice when the unique part of the state can be passed in externally rather than stored inside every object.

    7. The Proxy Design Pattern

    Real World Example

    Think of a security guard at the entrance of an office building. You can't walk straight into the building. You have to go through the guard first. The guard checks your name against the authorised list, logs your visit, and only then lets you through. If you're not on the list, you're turned away. The building itself never deals with any of that. It just lets people in. All the checking, logging, and decision-making happens at the guard (the proxy) before the building ever gets involved.

    That's the Proxy pattern. It sits between the caller and the real object, controls what gets through, and can add behaviour like access checks or logging without the real object knowing anything about it.

    Problems it solves:

    • What if anyone could walk straight into the building? There would be no access control. The proxy intercepts every request and decides whether it should be allowed through.

    • What if you need to log every entry without changing the building? The proxy handles it. The real building stays simple and focused on its own job.

    • What if the real object is expensive to create and you want to delay that? The proxy can hold off creating it until someone actually passes the check and needs it.

    In simple terms, you place an object in front of another object to control access to it. The caller thinks it's talking directly to the real object, but the proxy is handling it first.

    Wikipedia describes it like this:

    "A proxy, in its most general form, is a class functioning as an interface to something else. The proxy could interface to anything: a network connection, a large object in memory, a file, or some other resource that is expensive or impossible to duplicate." (Source)

    Programming Example:

    The client talks to IBuilding. The SecurityGuard is the proxy: it implements the same interface, controls access, and only lets authorised visitors through to the OfficeBuilding.

    // The subject interface — the building and the proxy both implement this
    public interface IBuilding
    {
        void Enter(string visitorName);
    }
    
    // The real subject — the actual building, just grants entry
    public class OfficeBuilding : IBuilding
    {
        public void Enter(string visitorName)
        {
            Console.WriteLine($"Building: {visitorName} has entered.");
        }
    }
    
    // The proxy — the security guard controls who gets through
    public class SecurityGuard : IBuilding
    {
        private readonly OfficeBuilding _building          = new();
        private readonly List<string>   _authorisedVisitors = new() { "Alice", "Bob", "Carol" };
    
        public void Enter(string visitorName)
        {
            Console.WriteLine($"Guard: {visitorName} is requesting entry.");
    
            if (_authorisedVisitors.Contains(visitorName))
            {
                Console.WriteLine("Guard: ID verified. Access granted.");
                _building.Enter(visitorName);
            }
            else
            {
                Console.WriteLine($"Guard: {visitorName} is not on the list. Access denied.");
            }
        }
    }
    

    Now let's see it in action:

    IBuilding entrance = new SecurityGuard();
    
    entrance.Enter("Alice");
    Console.WriteLine();
    entrance.Enter("David");
    Console.WriteLine();
    entrance.Enter("Bob");
    

    Output:

    Guard: Alice is requesting entry.
    Guard: ID verified. Access granted.
    Building: Alice has entered.
    
    Guard: David is requesting entry.
    Guard: David is not on the list. Access denied.
    
    Guard: Bob is requesting entry.
    Guard: ID verified. Access granted.
    Building: Bob has entered.
    

    The client called Enter() on what it thought was the building. It was actually the security guard. The guard decided what happened. The building only ever saw the people who were allowed through. That's the Proxy.

    When to Use it

    Use Proxy when you need access control, like only letting certain callers through to the real object.

    It's a good fit when you want to add behaviour such as logging, caching, or validation without changing the real object.

    And you can use it when the real object is expensive to create and you want to delay or guard that creation until it's truly needed.

    Behavioral Design Patterns

    Simply put, behavioral patterns are all about how objects communicate and share responsibility. They focus on the assignment of responsibilities between objects, and how objects cooperate to get a job done.

    Wikipedia describes them as:

    "In software engineering, behavioral design patterns are design patterns that identify common communication patterns among objects. By doing so, these patterns increase flexibility in carrying out this communication." (Source)

    Behavioral design patterns describe how objects interact and distribute responsibility, not just how they're structured. They focus on communication between objects: who talks to whom, and how much each side knows about the other.

    There are 11 behavioral design patterns:

    1. Chain of Responsibility: Passes a request along a chain of handlers, letting each one decide to handle it or pass it on.

    2. Command: Encapsulates a request as an object, letting you parameterize clients, queue actions, and support undo.

    3. Interpreter: Given a language, defines a representation for its grammar along with an interpreter that evaluates sentences in it.

    4. Iterator: Provides a way to access the elements of a collection sequentially without exposing how it's built underneath.

    5. Mediator: Defines an object that encapsulates how a set of objects interact, so they don't refer to each other directly.

    6. Memento: Captures an object's internal state so it can be restored later, without breaking encapsulation.

    7. Observer: Defines a one-to-many dependency so that when one object changes state, everything depending on it is notified automatically.

    8. State: Lets an object change its behaviour when its internal state changes, as if it had changed its class.

    9. Strategy: Defines a family of interchangeable algorithms and lets the client pick which one to use at runtime.

    10. Template Method: Defines the skeleton of an algorithm in a method, leaving some steps for subclasses to fill in.

    11. Visitor: Lets you define a new operation without changing the classes of the elements it operates on.

    1. The Chain of Responsibility Design Pattern

    Real World Example

    Think of an expense approval process at a company. An employee submits a request to their director. If the amount is small enough, the director approves it and that's the end of it. If it's too large for the director to sign off on, it goes up to the vice president. If it's still too large, it goes up to the chief executive.

    Each person in the chain only needs to know two things: what they're allowed to approve, and who to hand it to if they can't. The employee never needs to know who ends up approving it.

    That's the Chain of Responsibility design pattern. A request travels along a chain of handlers until one of them deals with it, and each handler only cares about its own link in that chain.

    Problems it solves:

    • What if the sender had to know exactly who should handle the request? That would tie the sender to a specific handler and break the moment the approval structure changed. The chain lets the sender submit the request without knowing who will end up handling it.

    • What if one handler could only ever approve or reject, with no fallback? Requests that fell outside its authority would simply fail. The chain lets a handler pass what it can't deal with further along.

    • What if you needed to change the approval structure? Rewiring which handler comes after which is enough. Neither the sender nor the other handlers need to change.

    In simple terms, you pass a request along a chain of handlers. Each handler decides whether to deal with it or hand it off to the next one in line.

    Wikipedia describes it like this:

    "In object-oriented design, the chain-of-responsibility pattern is a behavioral design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain."

    (Source)

    Programming Example:

    Each Approver knows its own approval limit and holds a reference to the next approver in the chain. The ExpenseRequest is passed along until someone can approve it, or nobody can.

    // The request that travels along the chain
    public class ExpenseRequest
    {
        public string Description { get; }
        public decimal Amount     { get; }
    
        public ExpenseRequest(string description, decimal amount)
        {
            Description = description;
            Amount      = amount;
        }
    }
    
    // The handler, every link in the chain implements this
    public abstract class Approver
    {
        private Approver? _next;
    
        public void SetNext(Approver next) => _next = next;
    
        public void Approve(ExpenseRequest request)
        {
            if (CanApprove(request))
            {
                Console.WriteLine($"{GetType().Name}: Approved '{request.Description}' (${request.Amount}).");
            }
            else if (_next is not null)
            {
                Console.WriteLine($"{GetType().Name}: Can't approve '{request.Description}' (${request.Amount}). Passing it up.");
                _next.Approve(request);
            }
            else
            {
                Console.WriteLine($"{GetType().Name}: No one left to approve '{request.Description}' (${request.Amount}). Request denied.");
            }
        }
    
        protected abstract bool CanApprove(ExpenseRequest request);
    }
    
    // Concrete handlers, each with its own approval limit
    public class Director : Approver
    {
        protected override bool CanApprove(ExpenseRequest request) => request.Amount <= 1000;
    }
    
    public class VicePresident : Approver
    {
        protected override bool CanApprove(ExpenseRequest request) => request.Amount <= 20000;
    }
    
    public class Chief : Approver
    {
        protected override bool CanApprove(ExpenseRequest request) => request.Amount <= 50000;
    }
    

    Now let's see it in action:

    Approver directorApprover = new Director();
    Approver vpApprover       = new VicePresident();
    Approver ceoApprover      = new Chief();
    
    directorApprover.SetNext(vpApprover);
    vpApprover.SetNext(ceoApprover);
    
    directorApprover.Approve(new ExpenseRequest("Laptop", 800));
    Console.WriteLine();
    directorApprover.Approve(new ExpenseRequest("Team offsite", 12000));
    Console.WriteLine();
    directorApprover.Approve(new ExpenseRequest("New office lease", 90000));
    

    Output:

    Director: Approved 'Laptop' ($800).
    
    Director: Can't approve 'Team offsite' ($12000). Passing it up.
    VicePresident: Approved 'Team offsite' ($12000).
    
    Director: Can't approve 'New office lease' ($90000). Passing it up.
    VicePresident: Can't approve 'New office lease' ($90000). Passing it up.
    Chief: No one left to approve 'New office lease' ($90000). Request denied.
    

    The employee only ever talked to the director. Whether the director, the vice president, or the chief ended up approving it, was decided by the chain itself, not the employee.

    When to Use it

    Use Chain of Responsibility when more than one object might handle a request, and the handler isn't known in advance.

    It's also a good choice when you want to issue a request without specifying the receiver explicitly.

    And it's helpful when the set of handlers, and their order, should be configurable rather than hard-coded.

    2. The Command Design Pattern

    Real World Example

    Think of a universal remote control. Every button is programmed to do one specific thing: turn a light on, turn a light off, and so on. When you press a button, the remote doesn't know or care how the light actually works internally. It just triggers the action that button was set up to perform. And because each button's action is a self-contained thing, the remote can also press it in reverse, undoing what it just did.

    That's the Command pattern. A request "turn the light on" is wrapped up as its own object. The thing that triggers it doesn't need to know anything about how it's carried out.

    Problems it solves:

    • What if the button had to know exactly how the light worked? Every button would need to be rewritten if the light's internals changed. Wrapping the action as a command means the remote never touches those details.

    • What if you wanted to undo the last action? Without a command object there's nothing to reverse, only a completed side effect. Wrapping the action gives you something you can also unwind.

    • What if you wanted to queue actions, log them, or trigger them later? A plain method call happens immediately and leaves nothing behind. A command is an object, so it can be stored, queued, and replayed.

    In simple terms, you wrap a request up as an object, so the thing that triggers it doesn't need to know how it's carried out, and the action itself can be queued, logged, or undone.

    Wikipedia describes it like this:

    "The command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time." (Source)

    Programming Example:

    The RemoteControl is the invoker it only knows about ICommand. LightOnCommand and LightOffCommand are the concrete commands, each wrapping the Light receiver and the action to perform on it.

    // The command interface, every action implements this
    public interface ICommand
    {
        void Execute();
        void Undo();
    }
    
    // The receiver, the object that actually does the work
    public class Light
    {
        private readonly string _room;
    
        public Light(string room) => _room = room;
    
        public void On()  => Console.WriteLine($"{_room} light: turned on.");
        public void Off() => Console.WriteLine($"{_room} light: turned off.");
    }
    
    // Concrete commands, each wraps a receiver and an action
    public class LightOnCommand : ICommand
    {
        private readonly Light _light;
    
        public LightOnCommand(Light light) => _light = light;
    
        public void Execute() => _light.On();
        public void Undo()    => _light.Off();
    }
    
    public class LightOffCommand : ICommand
    {
        private readonly Light _light;
    
        public LightOffCommand(Light light) => _light = light;
    
        public void Execute() => _light.Off();
        public void Undo()    => _light.On();
    }
    
    // The invoker, it holds a command and triggers it without knowing what it does
    public class RemoteControl
    {
        private ICommand? _command;
    
        public void SetCommand(ICommand command) => _command = command;
    
        public void PressButton() => _command?.Execute();
        public void PressUndo()   => _command?.Undo();
    }
    

    Now let's see it in action:

    var livingRoomLight = new Light("Living Room");
    var remote          = new RemoteControl();
    
    remote.SetCommand(new LightOnCommand(livingRoomLight));
    remote.PressButton();
    
    remote.SetCommand(new LightOffCommand(livingRoomLight));
    remote.PressButton();
    
    Console.WriteLine();
    Console.WriteLine("Undoing last action...");
    remote.PressUndo();
    

    Output:

    Living Room light: turned on.
    Living Room light: turned off.
    
    Undoing last action...
    Living Room light: turned on.
    

    The remote never called _light.On() or _light.Off() directly. It called Execute() and Undo() on whatever command it was holding. That's the Command pattern: the request itself became an object.

    When to Use it

    Use the Command pattern when you want to parameterize objects with an action to perform, rather than hard-coding it.

    Reach for it when you need to queue, log, or support undo for requests.

    And consider it when you want to decouple the object that invokes an action from the object that knows how to perform it.

    3. The Interpreter Design Pattern

    Real World Example

    Think of a basic calculator reading an expression like (5 + 3) - 2. Nobody hardcodes a single method that handles every possible expression. Instead, the expression is broken down into small pieces: numbers and operation buttons, each of which knows how to evaluate itself and combine with the others. (5 + 3) - 2 becomes a subtraction of two things: the number 2, and the result of adding 5 and 3. Each piece only needs to know how to interpret itself.

    That's the Interpreter pattern. A grammar is represented as a tree of small objects, and each one knows how to evaluate its own little piece of it.

    Problems it solves:

    • What if you tried to evaluate an entire expression in one big method? It would grow unmanageable the moment the grammar got more complex. Breaking the grammar into small classes, one per rule, keeps each piece simple.

    • What if the grammar needed to grow? Adding a new operation, like multiplication, is just a new class. The existing pieces don't need to change.

    • What if the same expression needed to be evaluated more than once, or in different contexts? Because each piece is just an object, the same tree can be interpreted again without rebuilding it.

    In simple terms, you represent a grammar as a tree of small objects, where each object knows how to interpret its own piece of the expression.

    Wikipedia describes it like this:

    "In computer programming, the interpreter pattern is a design pattern that specifies how to evaluate sentences in a language. The basic idea is to have a class for each symbol (terminal or nonterminal) in a specialized computer language." (Source)

    Programming Example:

    Number is the terminal expression, a plain value. Add and Subtract are non-terminal expressions, each combining two other expressions. Every node, terminal or not, knows how to Interpret() itself.

    // The abstract expression, every node in the grammar implements this
    public abstract class Expression
    {
        public abstract int Interpret();
    }
    
    // A terminal expression, a plain number that needs no further interpretation
    public class Number : Expression
    {
        private readonly int _value;
    
        public Number(int value) => _value = value;
    
        public override int Interpret() => _value;
    }
    
    // Non-terminal expressions, each combines other expressions
    public class Add : Expression
    {
        private readonly Expression _left;
        private readonly Expression _right;
    
        public Add(Expression left, Expression right)
        {
            _left  = left;
            _right = right;
        }
    
        public override int Interpret() => _left.Interpret() + _right.Interpret();
    }
    
    public class Subtract : Expression
    {
        private readonly Expression _left;
        private readonly Expression _right;
    
        public Subtract(Expression left, Expression right)
        {
            _left  = left;
            _right = right;
        }
    
        public override int Interpret() => _left.Interpret() - _right.Interpret();
    }
    

    Now let's see it in action:

    // (5 plus 3) minus 2
    Expression expression = new Subtract(
        new Add(new Number(5), new Number(3)),
        new Number(2)
    );
    
    Console.WriteLine($"Result: {expression.Interpret()}");
    
    // (10 minus 4) plus (2 plus 2)
    Expression another = new Add(
        new Subtract(new Number(10), new Number(4)),
        new Add(new Number(2), new Number(2))
    );
    
    Console.WriteLine($"Result: {another.Interpret()}");
    

    Output:

    Result: 6
    Result: 10
    

    Nothing ever evaluated the whole expression at once. Subtract asked its own _left and _right to interpret themselves, and those asked their own children, all the way down to plain numbers. That's the Interpreter pattern: the grammar interprets itself, one small piece at a time.

    When to Use it

    Use Interpreter when you have a simple language or grammar to evaluate, and representing it as a tree of expressions keeps it manageable.

    It also works well when the grammar is relatively stable. For example, adding new rules means adding new classes, not rewriting existing ones.

    And it's useful when you would rather have many small, focused classes than one large method trying to parse and evaluate everything at once.

    4. The Iterator Design Pattern

    Real World Example

    Think of a bookshelf. You want to go through it one book at a time, from left to right, without needing to know whether the books are held in an array, multiple piles and stacks, or something else entirely. All you need is a way to ask "what's next?" and to know when you've reached the end. How the shelf actually stores its books internally is none of your concern.

    That's the Iterator pattern. It gives you a consistent way to step through a collection, one element at a time, without exposing how that collection is built underneath.

    Problems it solves:

    • What if the client had to know how the collection was stored internally to loop over it? Any change to that internal structure would break every piece of code that loops over it. The iterator hides that structure behind a simple "get next" interface.

    • What if you needed more than one traversal in progress at the same time? A single shared position wouldn't work. Each iterator keeps its own position, so multiple traversals can happen independently.

    • What if you wanted to loop over the collection using the language's own foreach? Implementing the iterator interface the language expects means your custom collection gets that support for free.

    In simple terms, you give a collection a way to be walked through, one element at a time, without exposing how it's actually built underneath.

    Wikipedia describes it like this:

    "In object-oriented programming, the iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container's elements." (Source)

    Programming Example:

    Bookshelf is the aggregate it exposes an IEnumerator<string> without revealing that it stores books in a List<string> internally. BookshelfIterator is the iterator that walks through them one at a time.

    // The aggregate, exposes an iterator without revealing how books are stored
    public class Bookshelf : IEnumerable<string>
    {
        private readonly List<string> _books = new();
    
        public void Add(string title) => _books.Add(title);
    
        public IEnumerator<string> GetEnumerator() => new BookshelfIterator(_books);
    
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
    
    // The iterator, walks the collection one book at a time
    public class BookshelfIterator : IEnumerator<string>
    {
        private readonly List<string> _books;
        private int _position = -1;
    
        public BookshelfIterator(List<string> books) => _books = books;
    
        public string Current => _books[_position];
    
        object IEnumerator.Current => Current;
    
        public bool MoveNext()
        {
            _position++;
            return _position < _books.Count;
        }
    
        public void Reset() => _position = -1;
    
        public void Dispose() { }
    }
    

    Now let's see it in action:

    var bookshelf = new Bookshelf();
    bookshelf.Add("Clean Code");
    bookshelf.Add("The Pragmatic Programmer");
    bookshelf.Add("Design Patterns");
    
    foreach (var book in bookshelf)
    {
        Console.WriteLine($"On the shelf: {book}");
    }
    

    Output:

    On the shelf: Clean Code
    On the shelf: The Pragmatic Programmer
    On the shelf: Design Patterns
    

    The foreach loop never touched the List<string> inside Bookshelf directly. It called MoveNext() and Current on the BookshelfIterator, one step at a time. That's the Iterator pattern: the traversal logic lives outside the collection itself.

    When to Use it

    Use Iterator when you want to traverse a collection without exposing its internal structure.

    It's also a good choice when you need to support multiple simultaneous traversals over the same collection.

    And try it when you want your custom collection to work with the language's built-in iteration syntax, like foreach.

    5. The Mediator Design Pattern

    Real World Example

    Think of an air traffic control tower. Planes don't radio each other directly to negotiate who lands first. That would be chaos: dozens of pilots all trying to coordinate with each other at once.

    Instead, every plane talks only to the tower. The tower knows the state of the runway and tells each plane what to do. The planes never need to know how many other planes are around, or what they're doing.

    That's the Mediator pattern. Instead of objects talking to each other directly, they all talk to one central object that coordinates them.

    Problems it solves:

    • What if every aircraft had to communicate directly with every other aircraft? The number of connections would explode as more aircraft joined, and each one would need to know about all the others. The mediator means each aircraft only needs to know about the tower.

    • What if the coordination logic was scattered across every object involved? Changing how landings get prioritised would mean touching every aircraft. With a mediator, that logic lives in one place.

    • What if you wanted to add a new aircraft to the system? It only needs to know how to talk to the tower. It doesn't need to be introduced to every other aircraft already in the sky.

    In simple terms, instead of letting objects talk to each other directly, you route all communication through one central object that knows how to coordinate them.

    Wikipedia describes it like this:

    "In software engineering, the mediator pattern defines an object that encapsulates how a set of objects interact. This pattern is considered to be a behavioral pattern due to the way it can alter the program's running behavior." (Source)

    Programming Example:

    ControlTower is the mediator. It's the only thing an Aircraft ever talks to. It decides whether a plane can land based on the state it holds, and no aircraft ever contacts another aircraft directly.

    // The mediator interface
    public interface IControlTower
    {
        void RequestLanding(Aircraft requester);
    }
    
    // The concrete mediator, coordinates all the aircraft instead of letting them talk to each other
    public class ControlTower : IControlTower
    {
        private readonly List<Aircraft> _aircraft = new();
        private bool _runwayFree = true;
    
        public void Register(Aircraft aircraft) => _aircraft.Add(aircraft);
    
        public void RequestLanding(Aircraft requester)
        {
            if (_runwayFree)
            {
                _runwayFree = false;
                Console.WriteLine($"Tower: Runway clear. {requester.Name}, you are cleared to land.");
            }
            else
            {
                Console.WriteLine($"Tower: Runway occupied. {requester.Name}, please hold your position.");
            }
        }
    }
    
    // The colleague, only ever talks to the mediator, never to other aircraft directly
    public class Aircraft
    {
        public string Name { get; }
    
        private readonly IControlTower _tower;
    
        public Aircraft(string name, IControlTower tower)
        {
            Name   = name;
            _tower = tower;
        }
    
        public void RequestLanding()
        {
            Console.WriteLine($"{Name}: Requesting permission to land.");
            _tower.RequestLanding(this);
        }
    }
    

    Now let's see it in action:

    var tower = new ControlTower();
    
    var flight101 = new Aircraft("Flight 101", tower);
    var flight202 = new Aircraft("Flight 202", tower);
    
    tower.Register(flight101);
    tower.Register(flight202);
    
    flight101.RequestLanding();
    flight202.RequestLanding();
    

    Output:

    Flight 101: Requesting permission to land.
    Tower: Runway clear. Flight 101, you are cleared to land.
    Flight 202: Requesting permission to land.
    Tower: Runway occupied. Flight 202, please hold your position.
    

    Flight 101 and Flight 202 never spoke to each other. Neither one even knows the other exists. Both only ever talked to the tower, and the tower decided what happened next. That's the Mediator pattern.

    When to Use it

    Use Mediator when a group of objects communicate in complex, tangled ways, and you want to centralise that communication.

    It's also helpful when you want to reuse objects independently, without them being locked together by direct references to each other.

    And reach for it when the way objects interact changes often, and you'd rather change it in one place than in every object involved.

    6. The Memento Design Pattern

    Real World Example

    Think of the undo history in a text editor. Every so often, the editor quietly takes a snapshot of what the document looks like. It doesn't ask the document to expose its internals to do this. It just captures a copy of the content at that moment.

    When you press undo, the editor hands that snapshot back, and the document restores itself to exactly how it was. The history keeps a pile of these snapshots, but it never looks inside them or changes them. It only stores them and hands them back.

    That's the Memento pattern. It lets you capture and restore an object's state without exposing how that state is structured internally.

    Problems it solves:

    • What if undo required exposing every private field of the document? That would break encapsulation, and any change to the document's internals would ripple out to whatever handles undo. The memento hides that structure inside an object only the document itself knows how to read.

    • What if the history needed to inspect or modify old snapshots? It shouldn't be able to. The caretaker only stores and returns mementos, it never reads or changes what's inside them.

    • What if you needed several restore points, not just one? Because each memento is just an object, they can be stacked, listed, or discarded, giving you as many restore points as you want to keep.

    In simple terms, you capture an object's state in a snapshot you can restore later, without exposing how that state is put together internally.

    Wikipedia describes it like this:

    "The memento pattern is a software design pattern that provides the ability to restore an object to its previous state (undo via rollback)."

    (Source)

    Programming Example:

    TextEditor is the originator: it creates EditorMemento snapshots of itself and can restore from one. History is the caretaker: it stores mementos on a stack without ever looking inside them.

    // The memento, an immutable snapshot of the editor's state
    public class EditorMemento
    {
        public string Content { get; }
    
        public EditorMemento(string content) => Content = content;
    }
    
    // The originator, creates and restores from mementos of its own state
    public class TextEditor
    {
        public string Content { get; private set; } = string.Empty;
    
        public void Write(string text) => Content += text;
    
        public EditorMemento Save() => new(Content);
    
        public void Restore(EditorMemento memento) => Content = memento.Content;
    }
    
    // The caretaker, stores mementos without ever looking inside them
    public class History
    {
        private readonly Stack<EditorMemento> _snapshots = new();
    
        public void Save(EditorMemento memento) => _snapshots.Push(memento);
    
        public EditorMemento Undo() => _snapshots.Pop();
    }
    

    Now let's see it in action:

    var editor  = new TextEditor();
    var history = new History();
    
    editor.Write("Hello");
    history.Save(editor.Save());
    
    editor.Write(", world");
    history.Save(editor.Save());
    
    editor.Write("!!!");
    Console.WriteLine($"Current: {editor.Content}");
    
    editor.Restore(history.Undo());
    Console.WriteLine($"After undo: {editor.Content}");
    
    editor.Restore(history.Undo());
    Console.WriteLine($"After undo: {editor.Content}");
    

    Output:

    Current: Hello, world!!!
    After undo: Hello, world
    After undo: Hello
    

    History never read or changed the text inside a snapshot. It just pushed mementos on and popped them off. Only TextEditor knew what to do with the content inside one. That's the Memento pattern.

    When to Use it

    Use Memento when you need undo/redo functionality and want to capture state without exposing an object's internals.

    It's also a good choice when taking a snapshot directly would break encapsulation by exposing private fields.

    And it's helpful when you want the object that stores history to stay dumb: like holding snapshots without knowing or caring what's inside them.

    7. The Observer Design Pattern

    Real World Example

    Think of subscribing to a YouTube channel. You don't sit there refreshing the page, checking if a new video has been uploaded. You subscribe once, and the moment the channel uploads something, you get notified automatically.

    The channel doesn't know or care what each subscriber does with that notification. It just knows it has a list of subscribers, and when something changes, it tells all of them.

    That's the Observer pattern. One object holds a list of dependents, and whenever its state changes, it notifies every one of them automatically.

    Problems it solves:

    • What if every subscriber had to keep checking the channel for updates? That would waste effort and add delay. The channel notifying its subscribers directly means they find out the moment it happens.

    • What if the channel had to know exactly what each subscriber wanted to do with a new video? It shouldn't need to. The channel only calls Notify(), each subscriber decides for itself what that means.

    • What if you wanted to add or remove subscribers at runtime? The channel doesn't need to change. It just keeps a list, and subscribing or unsubscribing only ever affects that list.

    In simple terms, one object keeps a list of dependents and automatically notifies all of them whenever its own state changes.

    Wikipedia describes it like this:

    "The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods." (Source)

    Programming Example:

    YouTubeChannel is the subject: it keeps a list of ISubscribers and notifies all of them whenever a video is uploaded. Subscriber is the concrete observer, deciding for itself what to do with that notification.

    // The observer interface, every subscriber implements this
    public interface ISubscriber
    {
        void Notify(string channelName, string videoTitle);
    }
    
    // The concrete observer
    public class Subscriber : ISubscriber
    {
        private readonly string _name;
    
        public Subscriber(string name) => _name = name;
    
        public void Notify(string channelName, string videoTitle)
        {
            Console.WriteLine($"{_name}: {channelName} just uploaded '{videoTitle}'!");
        }
    }
    
    // The subject, keeps track of its subscribers and notifies them of changes
    public class YouTubeChannel
    {
        private readonly string _name;
        private readonly List<ISubscriber> _subscribers = new();
    
        public YouTubeChannel(string name) => _name = name;
    
        public void Subscribe(ISubscriber subscriber)   => _subscribers.Add(subscriber);
        public void Unsubscribe(ISubscriber subscriber) => _subscribers.Remove(subscriber);
    
        public void UploadVideo(string title)
        {
            Console.WriteLine($"{_name}: Uploaded '{title}'.");
    
            foreach (var subscriber in _subscribers)
            {
                subscriber.Notify(_name, title);
            }
        }
    }
    

    Now let's see it in action:

    var channel = new YouTubeChannel("Code With Isaiah");
    
    var alice = new Subscriber("Alice");
    var bob   = new Subscriber("Bob");
    
    channel.Subscribe(alice);
    channel.Subscribe(bob);
    
    channel.UploadVideo("Design Patterns Explained");
    
    channel.Unsubscribe(bob);
    channel.UploadVideo("Understanding the Observer Pattern");
    

    Output:

    Code With Isaiah: Uploaded 'Design Patterns Explained'.
    Alice: Code With Isaiah just uploaded 'Design Patterns Explained'!
    Bob: Code With Isaiah just uploaded 'Design Patterns Explained'!
    Code With Isaiah: Uploaded 'Understanding the Observer Pattern'.
    Alice: Code With Isaiah just uploaded 'Understanding the Observer Pattern'!
    

    Once Bob unsubscribed, he stopped hearing about new uploads entirely. The channel never singled him out, it just no longer had him on the list it notifies. That's the Observer pattern.

    When to Use it

    Use Observer when a change to one object should automatically update an unknown number of others.

    It's also useful when you want objects to stay loosely coupled: the subject only knows about an observer interface, never concrete details.

    And it's a good option when the number of dependents can grow or shrink at runtime, such as subscribing and unsubscribing.

    8. The State Design Pattern

    Real World Example

    Think of an online order moving through its lifecycle: pending, then shipped, then delivered. What "moving to the next step" actually means is different at every stage. From pending it means handing the package to a courier. From shipped it means marking it as received. From delivered, there's nowhere left to go. Rather than one giant method full of if checks for every possible stage, each stage can just know what comes after it.

    That's the State pattern. The object's behaviour changes based on its current state, and each state knows how to transition to the next one.

    Problems it solves:

    • What if one method had to handle every stage with a long chain of conditionals? It would grow harder to follow every time a new stage was added. Giving each stage its own class keeps the logic for that stage self-contained.

    • What if adding a new stage meant editing that same giant method? It's easy to introduce a bug in an unrelated stage while doing so. A new state is just a new class, dropped in alongside the others.

    • What if the object needed to behave completely differently depending on where it was in its lifecycle? Delegating to the current state object means the context doesn't need to know the details. It just asks the current state what to do.

    In simple terms, you let an object change its behaviour by changing which state object it's currently holding, so the object appears to change how it acts as its state changes.

    Wikipedia describes it like this:

    "The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines." (Source)

    Programming Example:

    Order is the context: it holds whatever IOrderState it's currently in and delegates to it. Each concrete state, PendingState, ShippedState, DeliveredState, knows what the next state should be.

    // The state interface, every state implements this
    public interface IOrderState
    {
        void Next(Order order);
        string Name { get; }
    }
    
    // The context, delegates behaviour to whatever state it currently holds
    public class Order
    {
        public IOrderState State { get; set; } = new PendingState();
    
        public void Next()
        {
            Console.WriteLine($"Order is currently: {State.Name}");
            State.Next(this);
        }
    }
    
    // Concrete states, each knows what comes after it
    public class PendingState : IOrderState
    {
        public string Name => "Pending";
    
        public void Next(Order order) => order.State = new ShippedState();
    }
    
    public class ShippedState : IOrderState
    {
        public string Name => "Shipped";
    
        public void Next(Order order) => order.State = new DeliveredState();
    }
    
    public class DeliveredState : IOrderState
    {
        public string Name => "Delivered";
    
        public void Next(Order order)
        {
            Console.WriteLine("Order has already been delivered. Nothing left to do.");
        }
    }
    

    Now let's see it in action:

    var order = new Order();
    
    order.Next();
    order.Next();
    order.Next();
    order.Next();
    

    Output:

    Order is currently: Pending
    Order is currently: Shipped
    Order is currently: Delivered
    Order has already been delivered. Nothing left to do.
    

    Order never checked "if pending, do this, if shipped, do that." It just asked its current state what to do next, and the state itself decided what came after. That's the State pattern.

    When to Use it

    Use State when an object's behaviour depends on its state, and it must change that behaviour at runtime as the state changes.

    It's also helpful when you have large conditional blocks that branch on the object's current state or type.

    And choose it when transitions between states should be explicit and self-contained, rather than scattered across one big method.

    9. The Strategy Design Pattern

    Real World Example

    Think of checking out of an online store. You can pay by credit card, or you can pay through PayPal. The shopping cart doesn't care which one you pick. It just knows the total, hands it to whichever payment method you chose, and lets that method handle the details of actually charging you. Swap the payment method, and the cart's own code never changes.

    That's the Strategy pattern. An algorithm (in this case "how to pay") is pulled out into its own interchangeable object, and the client just picks which one to use.

    Problems it solves:

    • What if the cart had a big if/else for every payment method? Adding a new one would mean editing that method every time. Pulling each payment method out into its own class means the cart never needs to change.

    • What if you wanted to swap the algorithm at runtime? A hardcoded method can't be swapped. A strategy object can simply be replaced with another one that implements the same interface.

    • What if two different payment methods needed to share a common interface but nothing else? Each one implements the strategy interface, but its internal details (a card number here, an email there) stay private to it.

    In simple terms, you pull an algorithm out into its own interchangeable object, so the class using it doesn't need to know or care which specific version is running.

    Wikipedia describes it like this:

    "The strategy pattern is a behavioral software design pattern that enables selecting an algorithm at runtime." (Source)

    Programming Example:

    ShoppingCart is the context: it holds an IPaymentStrategy and delegates the actual payment to it. CreditCardPayment and PayPalPayment are concrete strategies, each a different way to pay.

    // The strategy interface, every payment method implements this
    public interface IPaymentStrategy
    {
        void Pay(decimal amount);
    }
    
    // Concrete strategies, each a different way to pay
    public class CreditCardPayment : IPaymentStrategy
    {
        private readonly string _cardNumber;
    
        public CreditCardPayment(string cardNumber) => _cardNumber = cardNumber;
    
        public void Pay(decimal amount)
        {
            Console.WriteLine($"Charged ${amount} to credit card ending in {_cardNumber[^4..]}.");
        }
    }
    
    public class PayPalPayment : IPaymentStrategy
    {
        private readonly string _email;
    
        public PayPalPayment(string email) => _email = email;
    
        public void Pay(decimal amount)
        {
            Console.WriteLine($"Charged ${amount} via PayPal account {_email}.");
        }
    }
    
    // The context, holds a strategy and delegates the actual payment work to it
    public class ShoppingCart
    {
        private readonly decimal _total;
        private IPaymentStrategy? _paymentMethod;
    
        public ShoppingCart(decimal total) => _total = total;
    
        public void SetPaymentMethod(IPaymentStrategy method) => _paymentMethod = method;
    
        public void Checkout()
        {
            if (_paymentMethod is null)
            {
                Console.WriteLine("No payment method selected.");
                return;
            }
    
            _paymentMethod.Pay(_total);
        }
    }
    

    Now let's see it in action:

    var cart = new ShoppingCart(59.99m);
    
    cart.SetPaymentMethod(new CreditCardPayment("4111 1111 1111 1111"));
    cart.Checkout();
    
    cart.SetPaymentMethod(new PayPalPayment("isaiah@example.com"));
    cart.Checkout();
    

    Output:

    Charged $59.99 to credit card ending in 1111.
    Charged $59.99 via PayPal account isaiah@example.com.
    

    ShoppingCart never knew how a payment actually got processed. It just called Pay() on whatever strategy it was holding at the time. That's the Strategy pattern: the algorithm is swapped out, the class using it stays exactly the same.

    When to Use it

    Use Strategy when you have several variants of an algorithm, and want to switch between them at runtime.

    It's also helpful when you want to avoid a class full of conditionals that pick behaviour based on a type or flag.

    And it's a solid choice when related classes only differ in the behaviour they use, and that behaviour should be interchangeable.

    10. The Template Method Design Pattern

    Real World Example

    Think of making a hot drink, tea or coffee. Both follow the exact same basic steps: boil water, brew, pour into a cup, and add something to taste. What differs is only two of those steps: tea gets steeped, and coffee gets brewed through grounds. Tea gets lemon, and coffee gets sugar and milk. The overall recipe never changes, only the specific details of a couple of steps within it.

    That's the Template Method pattern. A base class defines the fixed skeleton of an algorithm, and subclasses only fill in the steps that are actually allowed to vary.

    Problems it solves:

    • What if every beverage repeated the entire recipe from scratch? Boiling water and pouring into a cup would be duplicated in every single class. The template method keeps those steps in one place, written once.

    • What if a subclass could reorder the steps, or skip one entirely? That would let each beverage break the overall recipe. Because the algorithm's skeleton lives in the base class as a single method, the order and structure stay fixed.

    • What if you wanted to add a new beverage? Only the steps that differ, brewing and condiments, need to be written. Everything else is already handled by the base class.

    In simple terms, you define the fixed skeleton of an algorithm in a base class, and let subclasses fill in only the steps that are actually allowed to differ.

    Wikipedia describes it like this:

    "In object-oriented programming, the template method is one of the behavioral design patterns identified by Gamma et al. in the book Design Patterns. The template method is a method in a superclass, usually an abstract superclass, and defines the skeleton of an operation in terms of a number of high-level steps." (Source)

    Programming Example:

    Beverage defines Prepare() as the template method: the fixed sequence of steps. Tea and Coffee only override Brew() and AddCondiments(), the two steps that are actually allowed to vary.

    // The abstract class, defines the skeleton of the algorithm
    public abstract class Beverage
    {
        // The template method, the steps and their order never change
        public void Prepare()
        {
            BoilWater();
            Brew();
            PourInCup();
            AddCondiments();
        }
    
        private void BoilWater() => Console.WriteLine("Boiling water.");
        private void PourInCup() => Console.WriteLine("Pouring into cup.");
    
        // Steps left for subclasses to fill in
        protected abstract void Brew();
        protected abstract void AddCondiments();
    }
    
    // A concrete class, fills in the steps specific to tea
    public class Tea : Beverage
    {
        protected override void Brew() => Console.WriteLine("Steeping the tea bag.");
        protected override void AddCondiments() => Console.WriteLine("Adding lemon.");
    }
    
    // Another concrete class, fills in the steps specific to coffee
    public class Coffee : Beverage
    {
        protected override void Brew() => Console.WriteLine("Brewing the coffee grounds.");
        protected override void AddCondiments() => Console.WriteLine("Adding sugar and milk.");
    }
    

    Now let's see it in action:

    Beverage tea    = new Tea();
    Beverage coffee = new Coffee();
    
    tea.Prepare();
    Console.WriteLine();
    coffee.Prepare();
    

    Output:

    Boiling water.
    Steeping the tea bag.
    Pouring into cup.
    Adding lemon.
    
    Boiling water.
    Brewing the coffee grounds.
    Pouring into cup.
    Adding sugar and milk.
    

    Both drinks boiled water and poured into a cup in exactly the same way, because Prepare() in the base class handled that. Only brewing and condiments changed, because those were the steps each subclass was actually responsible for. That's the Template Method pattern.

    When to Use it

    Use Template Method when several classes share the same overall algorithm, but differ in a few specific steps.

    It's a good choice when you want to enforce a fixed sequence of steps, while still letting subclasses customise parts of it.

    And it's helpful when you want to avoid duplicating the parts of an algorithm that never change across every subclass.

    11. The Visitor Design Pattern

    Real World Example

    Think of a shopping cart with different kinds of items: books and electronics, each taxed differently at checkout. You don't want to bake pricing logic into the Book and Electronic classes themselves, especially if you'll need other operations on them later too, like generating a shipping label or a warranty summary. Instead, each item just accepts a visitor and hands itself over. The visitor is the one that actually knows how to price a book differently from an electronic.

    That's the Visitor pattern. The operation lives outside the objects it acts on, and each object just lets the visitor know what it needs to know: what type of thing it actually is.

    Problems it solves:

    • What if pricing logic was written directly inside Book and Electronic? Every new operation (tax, shipping, warranty) would mean editing both classes again and again. The visitor keeps each new operation in its own self-contained class instead.

    • What if you needed to add a new operation without touching the existing item classes? Normally that means modifying every class the operation applies to. A new visitor is a new class, while Book and Electronic never change.

    • What if a generic loop had to guess the concrete type of each item? That usually means a chain of type checks. Accept() calling Visit(this) lets the compiler pick the right overload automatically, without a single if or type check.

    In simple terms, you move an operation out of the objects it acts on and into its own class. Each object just accepts a visitor and lets it know what concrete type it is.

    Wikipedia describes it like this:

    "The visitor design pattern is a way of separating an algorithm from an object structure on which it operates." (Source)

    Programming Example:

    Book and Electronic both implement IItem and simply call visitor.Visit(this). PricingVisitor implements IVisitor with an overload for each concrete type, so the right pricing logic runs automatically.

    // The element interface, every item in the cart implements this
    public interface IItem
    {
        void Accept(IVisitor visitor);
    }
    
    // Concrete elements, each accepts a visitor and hands itself over
    public class Book : IItem
    {
        public string Title { get; }
        public decimal Price { get; }
    
        public Book(string title, decimal price)
        {
            Title = title;
            Price = price;
        }
    
        public void Accept(IVisitor visitor) => visitor.Visit(this);
    }
    
    public class Electronic : IItem
    {
        public string Name { get; }
        public decimal Price { get; }
    
        public Electronic(string name, decimal price)
        {
            Name  = name;
            Price = price;
        }
    
        public void Accept(IVisitor visitor) => visitor.Visit(this);
    }
    
    // The visitor interface, one Visit overload per concrete element
    public interface IVisitor
    {
        void Visit(Book book);
        void Visit(Electronic electronic);
    }
    
    // A concrete visitor, adds a new operation without touching Book or Electronic
    public class PricingVisitor : IVisitor
    {
        public decimal Total { get; private set; }
    
        public void Visit(Book book)
        {
            Console.WriteLine($"Book: {book.Title} — ${book.Price:F2} (no tax).");
            Total += book.Price;
        }
    
        public void Visit(Electronic electronic)
        {
            var priceWithTax = electronic.Price * 1.15m;
            Console.WriteLine($"Electronic: {electronic.Name} — ${priceWithTax:F2} (with 15% tax).");
            Total += priceWithTax;
        }
    }
    

    Now let's see it in action:

    var cart = new List<IItem>
    {
        new Book("Design Patterns", 45.00m),
        new Electronic("Headphones", 120.00m)
    };
    
    var pricingVisitor = new PricingVisitor();
    
    foreach (var item in cart)
    {
        item.Accept(pricingVisitor);
    }
    
    Console.WriteLine($"Total: ${pricingVisitor.Total:F2}");
    

    Output:

    Book: Design Patterns — $45.00 (no tax).
    Electronic: Headphones — $138.00 (with 15% tax).
    Total: $183.00
    

    Neither Book nor Electronic contained a single line of pricing logic. Each one only knew how to Accept() a visitor. PricingVisitor was the one that actually decided how each type gets priced. That's the Visitor pattern: the operation lives outside the object structure, not inside it.

    When to Use it

    Use Visitor when you need to perform operations across a group of unrelated classes, without polluting each class with that logic.

    It's also helpful when you want to add new operations often, but the object structure itself rarely changes.

    And it's a good choice when you'd otherwise need type checks or casting to figure out what to do with each object in a collection.

    Conclusion

    That covers all 23 classic design patterns across the three families: Creational, Structural, and Behavioral.

    None of them are rules you must follow. They're answers to problems that show up again and again in software: how to create objects without hard-coding their exact type, how to compose bigger structures out of smaller ones, and how to let objects communicate without being tightly bound to each other.

    A few things worth remembering:

    • You won't use most of these patterns most of the time. Recognising when a problem calls for one is the actual skill. Forcing a pattern onto a problem that doesn't need it usually makes the code harder to follow, not easier.

    • The real world analogies exist to build intuition, not to be taken literally. Once a pattern's shape clicks in a story you understand, spotting it in real code becomes far easier.

    • Patterns compose. A Factory Method might produce objects that are themselves Decorators. A Composite tree might be built with a Builder. Real systems mix and layer patterns rather than using them in isolation.

    • The language doesn't matter. Every example here is in C#, but the same shapes exist in Python, Java, TypeScript, Go, Rust, and beyond. If you understand the problem a pattern solves, translating it to any language is straightforward.

    The goal isn't to memorise 23 names. It's to recognise the recurring problems underneath them, so that when one shows up in your own code, you already know a proven shape for solving it.

    "Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice."

    Source: Christopher Alexander, A Pattern Language — the architectural work that originally inspired software design patterns.

    If this handbook was useful, the source lives at github.com/Clifftech123/design-patterns-handbook. Star it, fork it, or open a PR with a pattern you think is missing.



    Read the whole story
    alvinashcraft
    22 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    In defense of the meeting that could have been an email

    1 Share

    We’ve all been in the meeting that could have been an email. And sure. Sometimes it absolutely should have been.

    If you need to tell me something, ask a quick question, send me a status update, or get a simple answer, please send the email. I don’t need 30 minutes on your calendar. You don’t need 30 minutes on mine.

    But somewhere along the way, “this meeting could have been an email” became shorthand for “this meeting was a waste of time.” I’m not sure those are the same thing anymore.

    Especially with AI in the mix.

    I’m not trying to have a relationship with you through Outlook. I’m trying to tell you the thing, answer the question, or get the information we need so we can both get back to work. My emails are very transactional. 

    Copilot’s coaching feature occasionally seems concerned about this. Apparently, I could be friendlier.  Maybe. But Outlook still doesn’t have a sarcasm font.

    When I’m actually talking to you, I can ask how your weekend was. And you can actually answer me.

    I can hear whether you’re excited about an idea or merely agreeing to it. I can catch the hesitation before you say yes. You can see that I’m joking instead of wondering whether that sentence was passive-aggressive. We can interrupt each other, clarify something immediately, change direction, laugh, disagree, and figure out what we actually mean.

    That matters.

    Most of the work still doesn’t need to happen in the meeting.

    Give me 30 minutes with the people who actually need to contribute to the conversation. Let’s get to a shared understanding of what we’re doing and why. Then we can all go away and do the work asynchronously.

    And AI makes that 30 minutes different than it used to be. AI can take the notes. It can summarize the conversation. It can pull out decisions and action items. It can give the people who weren’t there a way to catch up afterward.

    We don’t need another person in the virtual room just because somebody has to take notes. We don’t need to invite everyone who might eventually need the information just so they can hear it firsthand. We don’t all need to spend the meeting half-listening while trying to capture what was said.

    Which means we can be much more deliberate about who is there.

    Invite the people who contribute. And then let them contribute.

    Listen instead of transcribing. Watch the reactions. Ask the follow-up question. Notice when three people are using the same word and clearly mean three different things. Spend five minutes talking about someone’s weekend if that’s what humans who work together happen to do.

    Then send the terse email tomorrow.

    Sure. The meeting could have been an email.  But the email could also have been six emails, three misunderstandings, and a meeting next Thursday to sort it all out.

    Sometimes 30 minutes together is the faster way to understanding.

    Especially now that nobody has to take notes.

    Read the whole story
    alvinashcraft
    28 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    Run temporary containers in .NET tests with Meziantou.Framework.TemporaryContainers

    1 Share
    Integration tests are much more valuable when they run against real services. Instead of mocking a database or cache, you can start a temporary container, run your test, and dispose everything at the end. The Meziantou.Framework.TemporaryContainers package provides a lightweight API to manage disposable containers from .NET. Its main difference with the existing libraries is that it is not tied to Docker.…
    Read the whole story
    alvinashcraft
    39 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    Moving from Windows, .NET, and Visual Studio to Linux, Python, and Neovim

    1 Share
    A year ago I was celebrating 15 years in the .NET ecosystem and 30 years as a Windows user. Fast forward to today, I am using Linux, Neovim, Python, Rust, and mostly live in the terminal. I’m a solo developer, building a greenfield project, so I could have chosen any technology. Since I’m most experienced in Windows with C#, that would have been the more natural choice. Here’s my experience of why I decided to make different choices and whether it was the right call in hindsight.
    Read the whole story
    alvinashcraft
    57 seconds ago
    reply
    Pennsylvania, USA
    Share this story
    Delete

    Critter Stack Roadmap for September 2026

    1 Share

    JasperFx Software is closing in on a couple big releases in the next week or two that are worth talking about now to get last minute feedback and maybe additional requests.

    Critter Watch 1.1

    I’m hoping to have a follow up https://critterwatch.jasperfx.net release that builds on our 1.0 release with a handful of new features:

    1. Cron type scheduled recurring message publishing with a full dashboard for each management as well as matching MCP tools for AI agents. This builds on some recent Wolverine work that added recurring messaging. We looked at Quartz.Net, TickerQ, and Hangfire and decided that we could do what we needed to do for Wolverine and CritterWatch without taking on any explicit coupling. We’ll also be adding guides on what we think the best practices are for integrating Wolverine with the mature job scheduling tools out there, but we won’t be doing formal integration packages.
    2. Stream compacting policies for users to be able to specify recurring background jobs to automatically choose event streams for compacting based on user defined criteria (more than 1,000 events? older than 6 months?). We think this will be a great way to keep Marten/Polecat/Fisher applications performant over time. That will build on top of the Cron messaging.
    3. LLM callouts from CritterWatch alarm detection to use AI tools to potentially decide and carry out amelioration steps. This also builds on recent Wolverine additions for LLM integration.
    4. Scheduling projection rebuilds or subscription rewinds for Marten/Polecat/Fisher so you can have projection rebuilds happen during off hours.
    5. Bringing back the Embedded CritterWatch option to add CritterWatch to a single ASP.Net Core application instead of having to have a separate application. We think this will be very helpful for development time
    6. Moar robustness! One of our earliest users has a phenomenally large system with a complex multi-tenancy strategy that has given CritterWatch internals and user interface quite a workout
    7. Formal support for Event Modeling diagrams of configured and observed system behavior. To be more clear, this is Critter Watch being able to show you an Event Modeling visualization of the system as it actually is according to Wolverine configuration and observing the cause and effect between command messages and events appended or other messages being cascaded. This is part of our larger effort toward Event Modeling support.

    Event Modeling Support and Spec Driven Development

    Alright, this one is an arc across the entire Critter Stack to enable people who want to use Event Modeling, then use AI tools to scaffold or build applications using those models. We’re also working on a first class Spec Driven Development story for Critter Stack applications with and in addition to the Event Modeling tooling.

    Here’s a diagram of the three main ways we’re looking to support Event Modeling and Spec Driven Development across the Critter Stack, with a pair of newer critter tools that aren’t quite to 1.0 yet in “Stoat” and “Bobcat” (more on these below).

    The major pieces are:

    1. A “Semantic Model” in our low level JasperFx.Events library that models everything there is to a vertical slice according to Event Modeling semantics
    2. A new library called “Bobcat” that’s going to be our main Event Modeling visualization tool and Gherkin specification executor. It’s also a spiritual successor to my much older Storyteller project and has a lot of the same DNA for hopefully making automated integration testing more successful in enterprise systems. Bobcat also has command line helpers for generating skeleton Wolverine code for the event slice model
    3. Our JasperFx curated AI Skills that help constrain your AI tools to generate the cleanest and most idiomatic possible code you can with the Critter Stack as well as helping you choose the best Wolverine or Marten options for whatever your system needs. The AI Skills will also help you utilize the Critter Stack test automation support.
    4. A new tool named “Stoat” (no website yet, but we’re working on it!) that’s our “everything that makes AI usage more successful” tool including a feature set inspired by KurrentDb’s Capacitor that will add durable memory across your AI agents and coordination and visibility across AI agents. Stoat will be the controller for our entire Spec Driven Development story. Our thinking right now is that Stoat will be a commercial tool that will be bundled into AI Skills or CritterWatch purchases from JasperFx Software.

    Imagine a couple approaches to modeling an event driven system:

    1. Use the EventModelers.AI tooling for modeling. Stoat will be able to take the YAML file of the model exported from EventModelers.AI, then invoke AI agents and Bobcat to scaffold and develop a Critter Stack system from the model
    2. Using nothing but Gherkin to define the slice model for your system, including BDD style specifications, and have Bobcat create a running Event Modeling visualization of your system as well as enabling Stoat to again execute a development plan based on that model
    3. Lastly, and here’s where the Critter Stack is going to diverge quite a bit from seemingly the rest of the Event Sourcing community, a model where you can just write at least a skeleton of C# or F# types for events, read models, and command messages, then use a lightweight fluent interface in code to generate the Event Modeling visualization for easy review. I personally do not believe that the attempts to create intermediate DSLs for modeling event sourced systems like ESDM are going to be successful and that the Critter Stack’s very low code ceremony actually makes it easier to just write C# or F# code with a real IDE instead of futzing with DSLs.

    But, to that last point, I think we’re going to be well set up to potentially add support later for all those DSLs and YAML formats and even that really gnarly 2000’s era looking XML format going around in conferences by translating those to our own event slice model and working from there.

    And just to be clear, Stoat & the AI Skills and therefore our full Spec Driven Development strategy including our support for the EventModelers platform will be commercial add ons to the CritterStack. We’re still committed to “Open Core,” but these items are going to fall out of the MIT licensed core.

    I’m of the opinion that everybody’s approach to AI assisted development is exactly what their prior opinions were about software development. I was much more heavily influenced by Extreme Programming back in the day and have always been very dubious about any kind of “Model Driven Development” and that has carried through to being dubious about a lot of these new event modeling or spec driven development approaches. That’s also why you’re going to see us emphasize BDD style specifications more than modeling approaches.

    Extreme Scalability for Marten and Wolverine

    JasperFx has an ongoing effort with a large client that involves some rather extreme scalability requirements (10’s or 100’s of billions of events in a single system, 500+ different PostgreSQL databases, about 250 unique message types and that many HTTP web services. And a modular monolith to boot). We’ve already done quite a bit, with more ideas still to come. JasperFx and our client will be publishing a white paper sometime this year laying out all the challenges they faced and what we did to be able to enable their scale.

    I’m pretty excited for this to all come to fruition.

    Random Things?

    • More AI integration into Wolverine with the possibility of it maybe being a more durable option in place of Microsoft’s Agent Framework?
    • DuckDb integration for Marten, with Polecat and Fisher coming later

    Or suggestions or requests?

    Oh, and this is a stoat:



    Read the whole story
    alvinashcraft
    1 minute ago
    reply
    Pennsylvania, USA
    Share this story
    Delete
    Next Page of Stories