A curated registry of 22 classic design patterns demonstrated in C#. This guide gives you everything you need to build, run, and apply these patterns in production—along with decision questions to help you choose the right pattern for your scenario.
MythBuster: Contrary to outdated advice, design patterns are production-ready when applied judiciously. These examples mirror real-world use cases and prove that patterns can improve maintainability, extensibility, and clarity in enterprise systems.
- Prerequisites
- Getting Started
- Repository Layout
- Quick Reference
- Pattern Details
- Production Considerations
- License
- .NET 9 SDK installed
- A C#-capable IDE or editor (Visual Studio, Rider, VS Code, etc.)
-
Clone the repo:
git clone https://github.com/your-org/design-pattern-registry.git cd design-pattern-registry -
Build the solution:
dotnet build "Design Pattern Demos.sln" -
Select & Run a demo:
-
Open
Program.cs. -
Uncomment the desired
Demo.Run()orDemo.RunHTML()line. -
In terminal:
dotnet run --project "Design Pattern Demos"
-
├── Design Pattern Demos.sln
├── Program.cs # Entry point: toggle demos here
├── Patterns/
│ ├── Adapter/
│ │ └── Demo.cs
│ ├── Bridge/
│ │ └── Demo.cs
│ ├── Builder/
│ │ └── Demo.cs
│ ├── … (other patterns)…
│ └── Visitor/
│ └── Demo.cs
└── SOLID/ # Principle demos (SRP, OCP, etc.)
└── …
| Pattern | Intent | Demo Path |
|---|---|---|
| Adapter | Convert one interface to another | Patterns/Adapter/Demo.cs |
| Bridge | Decouple abstraction from implementation | Patterns/Bridge/Demo.cs |
| Builder | Construct complex objects step by step | Patterns/Builder/Demo.cs |
| Chain of Responsibility | Pass a request along a chain of handlers | Patterns/ChainOfResponsibility/Demo.cs |
| Command | Encapsulate requests as objects | Patterns/Command/Demo.cs |
| Composite | Compose objects into tree structures | Patterns/Composite/Demo.cs |
| Decorator | Add behavior dynamically via wrapping | Patterns/Decorator/Demo.cs |
| Facade | Provide a simple interface to a subsystem | Patterns/Facade/Demo.cs |
| Factory | Centralize object creation logic | Patterns/Factory/Demo.cs |
| Flyweight | Share common data among many objects | Patterns/Flyweight/Demo.cs |
| Interpreter | Define a grammar and interpret sentences | Patterns/Interpreter/Demo.cs |
| Iterator | Sequentially traverse a collection | Patterns/Iterator/Demo.cs |
| Mediator | Centralize complex communications | Patterns/Mediator/Demo.cs |
| Memento | Capture and restore object state | Patterns/Memento/Demo.cs |
| Null Object | Provide a do-nothing stand-in object | Patterns/NullObject/Demo.cs |
| Observer | Publish/subscribe for event handling | Patterns/Observer/Demo.cs |
| Prototype | Clone objects for efficient creation | Patterns/Prototype/Demo.cs |
| Proxy | Control access to another object | Patterns/Proxy/Demo.cs |
| Singleton | Ensure a single global instance | Patterns/Singleton/Demo.cs |
| State | Change behavior when internal state changes | Patterns/State/Demo.cs |
| Strategy | Swap algorithms at runtime | Patterns/Strategy/Demo.cs |
| Template Method | Define skeleton of an algorithm | Patterns/TemplateMethod/Demo.cs |
| Visitor | Add operations without modifying elements | Patterns/Visitor/Demo.cs |
-
Intent Convert the interface of a class into one that clients expect.
-
Context / When to Use Reusing legacy or third-party code whose interface doesn’t match your own.
-
Problem Client code depends on a specific interface; you cannot modify the existing class.
-
Solution Create an adapter that implements the target interface and delegates calls to the adaptee.
-
Benefits
- Allows code reuse without modifying existing classes
- Decouples client from service implementation
-
Demo
Patterns.Adapter.Demo.Run();
-
Decision Questions
- Do you need to integrate code whose interface you cannot change?
- Is there an impedance mismatch between two interfaces you must connect?
-
Intent Decouple an abstraction from its implementation.
-
Context / When to Use Both the abstractions and their implementations should vary independently.
-
Problem Class hierarchies explode when you combine multiple abstractions with multiple implementations.
-
Solution Define interfaces for abstraction and implementation, then compose them at runtime.
-
Benefits
- Extends abstractions and implementations independently
- Reduces coupling between high-level and low-level modules
-
Demo
Patterns.Bridge.Demo.Run();
-
Decision Questions
- Do you have orthogonal dimensions of variation in your classes?
- Would you benefit from swapping implementations at runtime?
-
Intent Construct complex objects step by step.
-
Context / When to Use Objects require numerous optional parameters or construction steps.
-
Problem Telescoping constructors or inconsistent object states are error-prone.
-
Solution Provide a builder with fluent methods to configure and then create the object.
-
Benefits
- Clear, readable construction code
- Ensures fully initialized, valid objects
-
Demo
Patterns.Builder.Demo.RunHTML();
-
Decision Questions
- Are you struggling with constructors that take too many parameters?
- Do you need to enforce mandatory steps or combinations before creation?
-
Intent Pass a request along a chain of handlers until one handles it.
-
Context / When to Use Multiple objects may handle a request, but the sender shouldn’t know which one does.
-
Problem Hard-coded if/else chains or tight coupling between sender and receivers.
-
Solution Link handler objects in a chain, each deciding to handle or forward the request.
-
Benefits
- Flexible assignment of responsibilities
- Loose coupling between senders and receivers
-
Demo
Patterns.ChainOfResponsibility.Demo.Run();
-
Decision Questions
- Do you have multiple potential handlers for the same request?
- Would you like to configure handler order dynamically?
-
Intent Encapsulate a request as an object.
-
Context / When to Use You need to parameterize clients with operations, support undo/redo, or queue commands.
-
Problem Caller is tightly coupled to the actions it invokes.
-
Solution Create command objects that implement an
Execute()method and possiblyUndo(). -
Benefits
- Supports undo/redo and logging
- Decouples sender and receiver
-
Demo
Patterns.Command.Demo.Run();
-
Decision Questions
- Do you need to queue, log, or undo operations?
- Do callers need to be decoupled from the execution logic?
-
Intent Compose objects into tree structures and treat them uniformly.
-
Context / When to Use You have part-whole hierarchies of objects.
-
Problem Clients must distinguish between leaf and composite objects.
-
Solution Define a common interface for leaves and containers, and compose recursively.
-
Benefits
- Simplifies client code
- Makes tree structures transparent to users
-
Demo
Patterns.Composite.Demo.Run();
-
Decision Questions
- Do you have recursive, tree-like object structures?
- Should clients treat individual objects and compositions uniformly?
-
Intent Add responsibilities to objects at runtime.
-
Context / When to Use You need flexible combinations of behaviors without an explosion of subclasses.
-
Problem Subclassing for every feature combination is unsustainable.
-
Solution Wrap the original object with decorator classes that implement the same interface.
-
Benefits
- Mix and match behaviors at runtime
- Open for extension, closed for modification
-
Demo
Patterns.Decorator.Demo.Run();
-
Decision Questions
- Do you need to add or remove features dynamically?
- Can you wrap objects instead of modifying their code?
-
Intent Provide a unified, simple interface to a complex subsystem.
-
Context / When to Use Clients must interact with multiple classes in a subsystem.
-
Problem Clients become tightly coupled to many subsystem classes and protocols.
-
Solution Create a facade class that exposes high-level operations and delegates internally.
-
Benefits
- Reduces coupling to subsystem details
- Simplifies client code
-
Demo
Patterns.Facade.Demo.Run();
-
Decision Questions
- Do clients need a simpler entry point to a complex API?
- Can you centralize coordination logic in one class?
-
Intent Centralize object creation logic.
-
Context / When to Use You need to decouple clients from concrete classes or vary products at runtime.
-
Problem Clients depend on constructors or concrete types.
-
Solution Provide a factory method or class that returns instances based on input.
-
Benefits
- Encapsulates creation logic
- Makes substitution of implementations easy
-
Demo
Patterns.Factory.Demo.Run();
-
Decision Questions
- Do you need to hide which concrete class you instantiate?
- Will you add new product types in the future?
-
Intent Share intrinsic state among many fine-grained objects.
-
Context / When to Use You must create large numbers of similar objects.
-
Problem Memory usage balloons when each object holds the same data.
-
Solution Store shared state in flyweight objects and pass unique context externally.
-
Benefits
- Significant memory savings
- Maintains object-oriented design
-
Demo
Patterns.Flyweight.Demo.Run();
-
Decision Questions
- Are you instantiating thousands of similar objects?
- Can you separate shared (intrinsic) from unique (extrinsic) state?
-
Intent Define a representation for a grammar and an interpreter.
-
Context / When to Use You have a language to evaluate or parse at runtime.
-
Problem Ad-hoc parsing code grows complex and hard to extend.
-
Solution Model grammar rules as classes with an
Interpret()method. -
Benefits
- Extensible grammar
- Cleaner parsing logic
-
Demo
Patterns.Interpreter.Demo.Run();
-
Decision Questions
- Do you need to interpret or evaluate expressions?
- Will your language or grammar evolve over time?
-
Intent Provide a way to access elements of a collection sequentially.
-
Context / When to Use You want to hide the internal structure of a collection.
-
Problem Clients depend on concrete collection types for traversal.
-
Solution Implement an iterator object with
MoveNext()andCurrent. -
Benefits
- Multiple, independent traversals
- Supports different traversal strategies
-
Demo
Patterns.Iterator.Demo.Run();
-
Decision Questions
- Do you need multiple cursors over a collection?
- Should clients be ignorant of the collection’s internals?
-
Intent Define an object that encapsulates how a set of objects interact.
-
Context / When to Use Many components communicate in complex ways.
-
Problem Direct references lead to a tangled web of interactions.
-
Solution Centralize communication in a mediator object.
-
Benefits
- Reduces coupling between colleagues
- Simplifies maintenance of interaction logic
-
Demo
Patterns.Mediator.Demo.Run();
-
Decision Questions
- Do components communicate in unpredictable ways?
- Would a central coordinator simplify your design?
-
Intent Capture and restore an object’s internal state without violating encapsulation.
-
Context / When to Use You need undo/rollback functionality.
-
Problem Storing state externally exposes internals.
-
Solution Originator creates a memento object; caretaker stores it for later restoration.
-
Benefits
- Encapsulated snapshots
- Clean undo stacks
-
Demo
Patterns.Memento.Demo.Run();
-
Decision Questions
- Do you need to rollback to previous states?
- Can you encapsulate all state needed into a memento?
-
Intent Provide a non-functional object to avoid null checks.
-
Context / When to Use You have many null checks scattered in client code.
-
Problem Clients guard against
nullbefore each use, cluttering code. -
Solution Implement a do-nothing object that conforms to the interface.
-
Benefits
- Eliminates null checks
- Simplifies client logic
-
Demo
Patterns.NullObject.Demo.Run();
-
Decision Questions
- Are you littering code with null guards?
- Can you replace
nullwith a harmless stub?
-
Intent Define a one-to-many subscription mechanism.
-
Context / When to Use Objects should be notified of state changes in other objects.
-
Problem Tight coupling or manual polling between subjects and observers.
-
Solution Observers register with a subject and receive automatic updates.
-
Benefits
- Loose coupling
- Supports event-driven designs
-
Demo
Patterns.Observer.Demo.Run();
-
Decision Questions
- Do multiple components react to the same events?
- Would you prefer push notifications over polling?
-
Intent Specify the kind of objects to create using a prototypical instance.
-
Context / When to Use Object creation is expensive or complex.
-
Problem Constructors with many parameters or resource-heavy initialization.
-
Solution Clone a prototype instance with a
Clone()method. -
Benefits
- Fast object creation
- Avoids constructor explosion
-
Demo
Patterns.Prototype.Demo.Run();
-
Decision Questions
- Is cloning cheaper than constructing from scratch?
- Can you deep-copy all necessary state reliably?
-
Intent Provide a surrogate for another object to control access.
-
Context / When to Use You need lazy loading, caching, or access control.
-
Problem Clients must handle initialization, security checks, or remote calls.
-
Solution Proxy implements the same interface and adds logic before delegating.
-
Benefits
- Transparent control over the real subject
- Supports lazy, remote, and security proxies
-
Demo
Patterns.Proxy.Demo.Run();
-
Decision Questions
- Do you need to defer object creation or enforce security?
- Can you hide the real subject behind a surrogate?
-
Intent Ensure a class has only one instance and provide a global access point.
-
Context / When to Use A single shared resource (e.g., configuration, logger).
-
Problem Multiple instances lead to inconsistent behavior or wasted resources.
-
Solution Use a static property that lazily creates and returns the sole instance.
-
Benefits
- Controlled single instance
- Lazy initialization
-
Demo
Patterns.Singleton.Demo.Run();
-
Decision Questions
- Do you absolutely need a single, shared instance?
- Could dependency injection be a better alternative?
-
Intent Allow an object to alter its behavior when its internal state changes.
-
Context / When to Use An object’s behavior must change in response to state transitions.
-
Problem Conditional statements for each state become unwieldy.
-
Solution Encapsulate state-specific behavior in separate classes implementing a common interface.
-
Benefits
- Removes bulky conditional logic
- Makes state transitions explicit
-
Demo
Patterns.State.Demo.Run();
-
Decision Questions
- Do you have many
if/elseorswitchblocks based on state? - Can you represent each state as a separate class?
- Do you have many
-
Intent Define a family of algorithms and make them interchangeable.
-
Context / When to Use A class uses one of several algorithms based on configuration or context.
-
Problem Hard-coded conditional logic to select algorithms.
-
Solution Encapsulate each algorithm in its own class and compose at runtime.
-
Benefits
- Simplifies adding new algorithms
- Promotes the Open/Closed Principle
-
Demo
Patterns.Strategy.Demo.Run();
-
Decision Questions
- Do you choose behavior via conditional logic?
- Would you benefit from swapping algorithms without code changes?
-
Intent Define the skeleton of an algorithm, deferring steps to subclasses.
-
Context / When to Use Multiple classes share the same algorithm structure but differ in steps.
-
Problem Duplicated code across similar algorithms.
-
Solution Implement an abstract base class with a template method that calls abstract operations.
-
Benefits
- Ensures consistent workflow
- Promotes code reuse
-
Demo
Patterns.TemplateMethod.Demo.Run();
-
Decision Questions
- Do you share an algorithm’s outline but vary individual steps?
- Can you extract common sequence logic into a base class?
-
Intent Represent an operation to be performed on elements of an object structure.
-
Context / When to Use You frequently add new operations on a fixed set of object classes.
-
Problem Adding behavior requires modifying every class in the structure.
-
Solution Create visitor classes with
Visit()methods for each element type; elements accept visitors. -
Benefits
- Easy to add new operations
- Keeps element classes stable
-
Demo
Patterns.Visitor.Demo.Run();
-
Decision Questions
- Do you need to add operations without changing element classes?
- Is your object structure stable but operations vary?
- Performance: Patterns add indirection. Measure and optimize critical paths.
- Thread Safety: Ensure singletons and shared flyweights are thread-safe.
- Dependency Injection: Combine factories, strategies, and singletons with DI containers.
- Maintainability: Overuse can over-engineer; apply patterns only when they solve concrete problems.
- Community & Frameworks: Many libraries (e.g., ASP.NET Core, EF Core) leverage these patterns—so can you!
This registry is released under the MIT License. Feel free to adapt and extend these demos for your teams and projects.