Support our educational content for free when you purchase through links on our site. Learn more
23 Design Patterns in C#: When to Use Each 🧩
Design patterns in C# are most useful when they solve a real, recurring design problem—not when they’re added just to make code look “architectural.” Learn the 23 classic patterns, understand the trade-offs, and choose the simplest approach that keeps your app or game easy to change.
We’ve seen a humble switch statement grow into a tangle of payment rules, notification options, or enemy behaviors. That’s often the moment a pattern like Strategy, Factory, or State can help—but the right choice depends on what’s changing and why.
The classic Gang of Four catalog groups its 23 patterns into creational, structural, and behavioral categories. In this guide, we’ll connect those ideas to modern C# and .NET, including dependency injection, ASP.NET Core, testing, and game development.
Key Takeaways
- Patterns are tools, not rules. Start with the design problem, then choose a pattern—or keep the simpler code.
- The 23 GoF patterns fall into three groups: creational, structural, and behavioral.
- Modern C# and .NET already provide useful building blocks, including dependency injection, events, delegates, generics, and iterators.
- Strategy, State, Observer, and Command are especially handy for changing behavior and event-driven gameplay.
- Patterns improve clarity and flexibility, not performance by default. Profile before optimizing.
- Avoid overengineering: add abstractions when they make a real change easier, safer, or more testable.
Table of Contents
- ⚡️ Quick Tips and Facts
- 🧭 C# Design Patterns: A Practical Guide
- What Design Patterns Are—and What They Aren’t
- Why Use Design Patterns in C# and .NET?
- A Brief History of Software Design Patterns
- 🧰 Getting Started with Design Patterns in C#
- Choosing the Right Pattern for the Problem
- C# Language Features That Make Patterns Easier
- SOLID Principles and Design Patterns
- 🏗️ Creational Design Patterns in C#
- 1. Singleton
- 2. Factory Method
- 3. Abstract Factory
- 4. Builder
- 5. Prototype
- 🧱 Structural Design Patterns in C#
- 6. Adapter
- 7. Bridge
- 8. Composite
- 9. Decorator
- 10. Facade
- 11. Flyweight
- 12. Proxy
- 🔄 Behavioral Design Patterns in C#
- 13. Chain of Responsibility
- 14. Command
- 15. Interpreter
- 16. Iterator
- 17. Mediator
- 18. Memento
- 19. Observer
- 20. State
- 21. Strategy
- 22. Template Method
- 23. Visitor
- 🧪 Applying Patterns in Real C# and .NET Applications
- Design Patterns in ASP.NET Core
- Dependency Injection and Service Lifetimes
- Patterns for Data Access, APIs, and Background Services
- Async, Thread Safety, and Performance Considerations
- ⚖️ How to Choose Between Common C# Patterns
- Factory vs. Builder
- Strategy vs. State
- Decorator vs. Proxy
- Adapter vs. Facade
- Observer vs. Mediator
- 🚫 Common Design Pattern Mistakes to Avoid
- 🛠️ Testing, Refactoring, and Maintaining Pattern-Based Code
- ✅ C# Design Patterns Quick Reference
- 🏁 Conclusion
- 🔗 Recommended Links
- ❓ FAQ
- What are the most commonly used design patterns in C#?
- Are design patterns built into C#?
- Which design pattern should I learn first?
- Is dependency injection a design pattern?
- Are design patterns still useful with modern .NET?
- Can design patterns make code harder to maintain?
- 📚 Reference Links
⚡️ Quick Tips and Facts
- Design patterns are reusable approaches, not plug-in code. They help teams talk about recurring design problems and possible solutions; adapt them to your app or game rather than copying a diagram blindly. Start with our guide to Software Design Patterns: 23 Proven Solutions 🧩.
- The classic Gang of Four (GoF) catalog contains 23 patterns: five creational, seven structural, and eleven behavioral. Refactoring.Guru’s C# catalog and Dofactory’s .NET catalog use these familiar groupings.
- Prefer the simplest design that meets the need. A couple of clear methods may better than introducing five interfaces and a factory that manufactures another factory.
- Use C# and .NET features naturally. Built-in dependency injection, delegates, events, generics, and collection interfaces can make classic pattern implementations smaller—or make a custom implementation unnecessary.
- Patterns can improve testability and flexibility, but they don’t automatically improve performance. Extra allocations, indirection, and abstraction can cost time or memory in hot paths.
- For games, patterns often help organize systems and changing behavior: Strategy for interchangeable algorithms, State for player or enemy modes, Observer for events, and Object Pooling when repeatedly creating and destroying objects is costly.
- Quick rule of thumb: identify what changes, then isolate that change. If nothing is changing and the code is already easy to understand, you may not need a pattern. ✅
| Fact | Practical takeaway |
|---|---|
| The GoF catalog has 23 patterns | Learn the problem each pattern solves, not just its class diagram |
| Patterns are language-independent ideas | C# features and .NET libraries often provide concise ways to apply them |
| More abstraction means more moving parts | Add a pattern when it reduces real complexity or makes change safer |
| A pattern name is not proof of good design | Clear requirements, tests, and maintainable code matter more |
🧭 C# Design Patterns: A Practical Guide
A design pattern is a repeatable way to structure code for a recurring design problem. It is not a framework, a mandatory rule, or a guarantee that your code will be elegant. Think of patterns as workshop tools: useful when they fit the job, awkward when you reach for them just because they’re shiny.
The original GoF catalog remains a useful shared vocabulary. Dofactory presents the 23 patterns in the familiar three categories, while Refactoring.Guru provides concise C#-focused explanations and examples. Those catalogs are broadly aligned on the pattern names and purposes. Differences usually come from how a pattern is implemented in a particular language or framework, not from a secret 24th GoF pattern hiding in the toolbox.
What Design Patterns Are—and What They Aren’t
A pattern describes a problem, a design approach, and the context where that approach can help. It does not dictate exact class names or require a specific number of interfaces.
As C# Corner puts it, “A pattern is a recurring solution to a problem in a context.” That last phrase matters: a solution that works in a payment service may be unnecessary ceremony in a one-screen utility app.
Pattern, principle, or architecture?
| Term | What it describes | Example |
|---|---|---|
| Design pattern | A reusable structure for a recurring local design problem | Strategy for interchangeable pricing rules |
| Design principle | A guideline for making design decisions | Single Responsibility Principle (SRP) |
| Architecture | The broader organization of an application or system | Layered architecture, clean architecture |
| Framework feature | A concrete capability provided by a library or runtime | ASP.NET Core dependency injection |
A pattern can help you apply a principle, but they are not interchangeable. For example, Strategy can support the Open/Closed Principle by allowing new algorithms to be added behind an abstraction.
Why Use Design Patterns in C# and .NET?
Patterns are most useful when they make a codebase easier to change, test, explain, or extend. A payment workflow with several providers may benefit from a Strategy or Factory approach. A small method that formats a label probably does not need a design-pattern committee meeting.
Benefits can include:
- Lower coupling: consumers rely on an interface or contract rather than a specific implementation.
- Clearer responsibilities: creation, behavior, and coordination can be separated.
- Easier testing: dependencies can be replaced with fakes or test doubles.
- Shared vocabulary: “use a decorator” can be faster than describing a wrapping design from scratch.
- Safer extension: a new implementation can be added without rewriting stable calling code.
There are trade-offs, too. Additional interfaces and objects can create indirection, increase the learning curve, and obscure a simple flow. The Microsoft .NET architecture guidance emphasizes choosing designs that suit the application’s needs; it does not suggest applying every pattern everywhere.
A Brief History of Software Design Patterns
The modern software-pattern conversation was strongly shaped by the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software, written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—the “Gang of Four.” Its 23 patterns gave object-oriented developers a durable vocabulary for recurring design challenges.
The catalog predates modern C# features and today’s .NET ecosystem. That means some examples can look more elaborate than the idiomatic equivalent using delegates, generics, built-in collections, or dependency injection. The underlying design problem may still be relevant even when the original class structure is not.
🧰 Getting Started with Design Patterns in C#
Before choosing a pattern, write down what is difficult about the current design. Is object creation scattered everywhere? Do several classes know too much about each other? Does behavior change according to state? A clear problem statement is more useful than a pattern name picked in advance.
Choosing the Right Pattern for the Problem
Use this sequence as a practical filter:
-
Describe the pain in plain language.
For example: “Every new notification provider forces edits across the order workflow.” -
Find what varies.
Is it object creation, an algorithm, a state transition, or communication between components? -
Check whether the change is recurring.
A one-off variation may not justify a generalized design. -
Compare the smallest plausible solutions.
Consider a method, a delegate, an interface, or a framework feature before building a hierarchy. -
Implement one vertical slice.
Test the real use case rather than building an abstract pattern zoo. -
Measure and review.
Check readability, testability, runtime cost, and whether the anticipated change actually became easier.
Here’s a quick guide:
| Design symptom | Patterns worth considering | First simpler option to check |
|---|---|---|
| Consumers instantiate many concrete types | Factory Method, Abstract Factory | A constructor or DI registration |
| The same construction steps create different representations | Builder | Named arguments or a factory function |
| Several algorithms must be selected at runtime | Strategy | A delegate or Func<T, TResult> |
| A subsystem is awkward to call | Facade | A small application service |
| Many objects need to react to a change | Observer, Mediator | .NET events or a message abstraction |
| An object’s behavior changes with its mode | State | An enum and a clear switch |
| Behavior needs to be added around an object | Decorator | Middleware or a wrapper |
| Repeated object creation causes measured overhead | Object Pooling (not a GoF pattern) | Profiling and allocation improvements |
C# Language Features That Make Patterns Easier
C# gives developers several ways to express patterns without turning every concept into a forest of classes.
| C# or .NET feature | How it can help |
|---|---|
| Interfaces | Define contracts for interchangeable implementations |
| Delegates and lambdas | Represent simple strategies, callbacks, and handlers |
| Events | Support publisher–subscriber notification |
| Generics | Build reusable components while retaining type safety |
| Records | Model immutable data and value-based equality |
| Pattern matching | Express type- or state-based decisions concisely |
| Built-in dependency injection | Register and provide application services |
IEnumerable<T> and IAsyncEnumerable<T> |
Iterate through data without exposing a collection’s internals |
For example, a simple strategy may need only a delegate:
Func<decimal, decimal> applyDiscount = total => total * 0.9m;
decimal discountedTotal = applyDiscount(100m);
``
If strategies need names, dependencies, configuration, or independent tests, separate classes and an interface may be clearer. Use the simplest representation that keeps the design understandable.
### SOLID Principles and Design Patterns
SOLID principles are **design guidelines**, not a pattern catalog. They can help you decide whether a pattern is improving the design or merely adding scaffolding.
- **Single Responsibility Principle (SRP):** give a type a focused reason to change.
- **Open/Closed Principle (OCP):** make likely extensions possible without repeatedly modifying stable code.
- **Liskov Substitution Principle (LSP):** implementations should honor the contracts their callers rely on.
- **Interface Segregation Principle (ISP):** avoid forcing clients to depend on methods they do not use.
- **Dependency Inversion Principle (DIP):** keep high-level policy from depending directly on low-level implementation details.
In the [featured video](#featured-video), the presenter demonstrates SOLID through an e-commerce notification example: an email sender is separated from notification orchestration, and future SMS or push implementations can follow a shared abstraction. The lesson is useful beyond interviews: **design for a change you can reasonably anticipate, not every change imaginable**. Keep secrets out of source code; Microsoft explains secure configuration approaches in its guidance on [app secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) and [Azure Key Vault configuration](https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration).
## 🏗️ Creational Design Patterns in C#
Creational patterns help control **how objects are created**. They can centralize complex construction or keep consumers independent of concrete types. They can also be overkill: if a plain constructor is obvious and stable, use the constructor.
| Pattern | Main purpose | Common C# use |
|---|---|---|
| Factory Method | Let a method or subclass choose a product type | Selecting a parser or exporter |
| Abstract Factory | Create related products as a compatible family | Platform-specific UI or game assets |
| Builder | Construct a complex object step by step | Request objects or configuration |
| Prototype | Create a new object by copying an existing one | Cloneable configurations or templates |
| Singleton | Restrict a type to one shared instance | Carefully scoped process-wide service |
### 1. Singleton
Singleton provides a shared access point to a single instance. It is easy to recognize and easy to misuse.
A basic thread-safe C# form uses `Lazy<T>`:
```csharp
public sealed class AppSettingsCache
{
private static readonly Lazy<AppSettingsCache> InstanceFactory =
new(() => new AppSettingsCache());
public static AppSettingsCache Instance => InstanceFactory.Value;
private AppSettingsCache()
{
}
``
.NET also supports singleton **service lifetimes** through dependency injection. In an ASP.NET Core application, registering a service with `AddSingleton` tells the container to reuse one instance for that service registration:
```csharp
builder.Services.AddSingleton<IClock, SystemClock>();
``
That is not identical to hard-coding a Singleton access point in a class. Dependency injection keeps the lifetime decision at the composition root and makes tests and substitutions easier. See Microsoft’s documentation on [dependency injection in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection).
**Benefits**
- Convenient sharing of stateless or carefully managed process-wide services.
- A single instance can avoid repeated setup when sharing is genuinely required.
**Drawbacks**
- Global mutable state makes tests order-dependent and can create hidden coupling.
- A singleton instance may become a concurrency bottleneck.
- “One per process” is not the same as “one across a cluster” or “one per multiplayer game session.”
**Our recommendation:** use DI-managed singleton lifetime for suitable services, keep such services thread-safe, and avoid using a singleton as a shortcut for passing state throughout the application.
### 2. Factory Method
Factory Method delegates product creation to a method or subclass, so the calling code can work with a shared contract.
```csharp
public interface IReportExporter
{
Task ExportAsync(Stream destination, CancellationToken cancellationToken);
}
public sealed class JsonExporter : IReportExporter
{
public Task ExportAsync(Stream destination, CancellationToken cancellationToken)
{
// Write a JSON report.
return Task.CompletedTask;
}
public static class ExporterFactory
{
public static IReportExporter Create(string format) =>
format.ToLowerInvariant() switch
{
"json" => new JsonExporter(),
_ => throw new NotSupportedException($"Unsupported format: {format}")
};
}
``
A simple static factory like this is useful, but it is not necessarily the full GoF Factory Method structure, which commonly uses an overridable creation method. Don’t worry about the label too much: care more that creation is localized and the caller depends on a useful abstraction.
**Use it when:** construction rules vary by input or you want callers insulated from implementation types.
**Watch out for:** giant factory `switch` statements that become the next maintenance problem. Registration, keyed services, or separate factories may fit better as product choices grow.
### 3. Abstract Factory
Abstract Factory creates **families of related objects** through a shared factory interface. It helps ensure that components belong together.
Imagine a game with two UI themes. A `DarkThemeFactory` creates a dark menu and dark button; a `LightThemeFactory` creates the matching light versions. The game UI depends on abstract products, not the concrete theme classes.
```csharp
public interface IMenu
{
string Render();
}
public interface IButton
{
string Render();
}
public interface IThemeFactory
{
IMenu CreateMenu();
IButton CreateButton();
}
``
**Factory Method vs. Abstract Factory:** Factory Method usually focuses on creating a product through one creation point. Abstract Factory coordinates the creation of **multiple related product types**. If your code only creates one kind of object, Abstract Factory may be unnecessary.
The [C# Corner guide](https://www.c-sharpcorner.com/article/design-patterns-in-net/) illustrates the family idea with alternate packaging and shipping-document implementations. That example conveys the central benefit: choose one factory and receive a coherent set of products. The trade-off is that adding a new product type can require updating every factory.
### 4. Builder
Builder separates **step-by-step construction** from the finished object. It is helpful when a type has many optional settings, validation rules, or construction stages.
```csharp
public sealed record GameSessionOptions(
string MapName,
int MaxPlayers,
bool EnableVoiceChat);
var options = new GameSessionOptions(
MapName: "Harbor",
MaxPlayers: 8,
EnableVoiceChat: false);
``
For a small immutable record, a constructor may be enough. A dedicated builder is more useful when the object has many configuration steps:
```csharp
public sealed class GameSessionOptionsBuilder
{
private string _mapName = "Default";
private int _maxPlayers = 4;
private bool _voiceChat;
public GameSessionOptionsBuilder OnMap(string mapName)
{
_mapName = mapName;
return this;
}
public GameSessionOptionsBuilder WithMaxPlayers(int count)
{
if (count < 1)
throw new ArgumentOutOfRangeException(nameof(count));
_maxPlayers = count;
return this;
}
public GameSessionOptionsBuilder EnableVoiceChat()
{
_voiceChat = true;
return this;
}
public GameSessionOptions Build() =>
new(_mapName, _maxPlayers, _voiceChat);
}
``
**Benefits:** readable construction, centralized validation, and support for alternate representations.
**Drawbacks:** builders add types and mutable intermediate state. If the result has only two required fields, a builder is probably wearing a hard hat to assemble a stool.
### 5. Prototype
Prototype creates new objects by copying an existing instance. It can be useful when creating an object from scratch is expensive or when a configured template is convenient.
In C#, copying needs care:
- **Shallow copy:** copies the object but keeps references to nested objects.
- **Deep copy:** also duplicates nested objects, according to an explicit policy.
- **Immutable data:** can often be shared safely, reducing the need for cloning.
- **Serialization-based cloning:** may be convenient but can be slow, fragile, and unsuitable as a general-purpose copying mechanism.
For a game, a level editor might duplicate a configured enemy template, then adjust its position and health. Consider an explicit copy method or a dedicated factory so it is clear which state is shared and which is duplicated.
## 🧱 Structural Design Patterns in C#
Structural patterns organize **how types and objects fit together**. In everyday C# development, decorators, adapters, and facades are especially easy to recognize.
| Pattern | Main purpose | Everyday example |
|---|---|---|
| Adapter | Make incompatible interfaces work together | Wrap a legacy payment SDK |
| Bridge | Separate abstractions from implementations | A renderer paired with platform-specific output |
| Composite | Treat individual items and groups uniformly | Scene graph or folder tree |
| Decorator | Add behavior around an existing object | Logging around an HTTP handler |
| Facade | Offer a simpler interface to a subsystem | A checkout service coordinating several components |
| Flyweight | Share common data to reduce duplication | Shared immutable terrain or glyph data |
| Proxy | Control access to another object | Lazy loading or remote access |
### 6. Adapter
An Adapter converts one interface into another expected by the caller.
Suppose a game uses `IAnalytics`, but a third-party SDK exposes a different API. An adapter implements the application’s interface and forwards calls to the SDK.
```csharp
public interface IAnalytics
{
void Track(string eventName);
}
public sealed class VendorAnalyticsAdapter : IAnalytics
{
private readonly VendorSdk _sdk;
public VendorAnalyticsAdapter(VendorSdk sdk) => _sdk = sdk;
public void Track(string eventName) => _sdk.SendEvent(eventName);
}
``
**Use it when:** integrating legacy code, third-party libraries, platform services, or external APIs without spreading vendor-specific calls throughout your app.
**Trade-off:** a thin adapter is useful; an adapter that silently changes behavior or hides important errors can make debugging harder.
### 7. Bridge
Bridge separates an abstraction from its implementation so both can vary independently. It can prevent a class hierarchy from multiplying across two changing dimensions.
For example, a game may have several notification types and several delivery channels. Instead of creating `WinScreenPushNotifier`, `WinScreenEmailNotifier`, `LossScreenPushNotifier`, and so on, a notification abstraction can hold a delivery implementation.
**Use it when:** two independent dimensions are both expected to grow.
**Avoid it when:** the hierarchy is small and stable. A Bridge can add more indirection than value when there is only one implementation and no realistic variation.
### 8. Composite
Composite arranges objects into trees and lets callers treat a leaf and a group through a common interface.
This is a natural fit for:
- Scene graphs in Unity or other game engines.
- UI hierarchies.
- File and folder structures.
- Nested expression trees.
- Groups of game objects that need common operations.
A composite design is particularly useful when clients should call a consistent operation on both individual objects and collections. Be careful with methods that make no sense on leaves—for example, an `AddChild` method on every scene node can force awkward exceptions or no-op behavior.
### 9. Decorator
Decorator wraps an object implementing the same interface and adds behavior without modifying the wrapped implementation.
```csharp
public interface IMessageSender
{
Task SendAsync(string message, CancellationToken cancellationToken);
}
public sealed class LoggingMessageSender : IMessageSender
{
private readonly IMessageSender _inner;
private readonly ILogger<LoggingMessageSender> _logger;
public LoggingMessageSender(
IMessageSender inner,
ILogger<LoggingMessageSender> logger)
{
_inner = inner;
_logger = logger;
}
public async Task SendAsync(string message, CancellationToken cancellationToken)
{
_logger.LogInformation("Sending a message");
await _inner.SendAsync(message, cancellationToken);
}
``
**Benefits:** combine optional behavior such as logging, caching, retry policies, and metrics without creating a subclass for every combination.
**Drawbacks:** layers can become difficult to trace, and order matters. A retry decorator outside a logging decorator may produce different logs than the reverse arrangement.
.NET’s [HTTP client factory](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) uses a handler pipeline that offers a related way to compose cross-cuting HTTP behavior.
### 10. Facade
Facade provides a **simplified entry point** to a more complex subsystem.
A checkout facade might coordinate cart validation, payment authorization, inventory reservation, and order creation. The calling UI can depend on a single application-level operation instead of orchestrating every detail.
**Benefits**
- Hides subsystem complexity from callers.
- Offers a natural place to enforce a workflow.
- Helps keep UI or controller code focused.
**Trade-offs**
- A facade can grow into a “god object” if it accumulates unrelated responsibilities.
- It should coordinate the subsystem, not swallow every business rule in the application.
### 11. Flyweight
Flyweight reduces memory use by sharing common, relatively stable state among many objects. The [Refactoring.Guru Flyweight guide](https://refactoring.guru/design-patterns/flyweight) emphasizes shared state as its core idea.
In a game, thousands of trees may share a mesh, material, and species data while each instance holds its own position and health. Keep shared and per-instance state distinct:
| State kind | Example | Likely owner |
|---|---|---|
| Shared | Mesh, texture, immutable species properties | Flyweight or asset resource |
| Per instance | Position, damage, animation phase | Individual game object |
**Important:** modern engines may already provide asset sharing, instancing, or batching. Profile first. Introducing a custom Flyweight system without measuring can add complexity while saving no meaningful memory.
### 12. Proxy
A Proxy stands in for another object and controls access to it. It can defer work, enforce checks, cache results, or represent a remote service.
Common examples include:
- Lazy-loading a large resource.
- Checking authorization before a call.
- Caching an expensive result.
- Representing a remote API through a local interface.
A proxy should preserve the expected contract. If it changes timing, failure behavior, or security semantics, make those differences explicit rather than surprising callers.
## 🔄 Behavioral Design Patterns in C#
Behavioral patterns focus on **communication, responsibilities, and how behavior changes**. They are particularly useful in event-driven apps and gameplay systems, where objects often respond to user actions, system events, or changing game state.
| Pattern | Main idea | Good starting question |
|---|---|---|
| Chain of Responsibility | Pass a request through handlers | Which handler can process this request? |
| Command | Represent an action as an object | Do I need to queue, log, replay, or undo this action? |
| Interpreter | Evaluate a small language or grammar | Am I building a domain-specific expression language? |
| Iterator | Traverse a collection through a common interface | Can callers enumerate without knowing storage details? |
| Mediator | Coordinate communication through a hub | Are components becoming tangled with direct references? |
| Memento | Save and restore prior state | Do I need undo, checkpoints, or snapshots? |
| Observer | Notify subscribers about an event | Should several listeners react to a change? |
| State | Change behavior according to current state | Is a growing conditional tied to an object’s lifecycle? |
| Strategy | Swap interchangeable algorithms | Can the same task be performed in different ways? |
| Template Method | Define a fixed algorithm skeleton with overridable steps | Do subclasses share the same workflow but vary at specific steps? |
| Visitor | Add operations across a stable object structure | Must new operations be added without changing element types? |
### 13. Chain of Responsibility
A request travels through a sequence of handlers. Each handler can process it or pass it along.
This pattern suits validation pipelines, middleware, and request processing. ASP.NET Core middleware is a familiar pipeline model: each component can act on the request, call the next component, or short-circuit processing. Microsoft documents the model in [ASP.NET Core middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/).
**Benefits:** handlers can be reordered or added without placing all logic in one method.
**Drawbacks:** requests can reach the end unhandled, and the actual processing path may be harder to trace. Make ordering and termination behavior clear.
### 14. Command
Command packages an action and its data as an object. That makes actions easier to queue, log, retry, schedule, or undo.
For a game, `MovePlayerCommand`, `FireWeaponCommand`, and `UseItemCommand` can be recorded and replayed. In an app, a command may represent “submit order” or “change subscription.”
A command pattern does **not automatically make an action reversible**. Undo requires recording enough prior state to reverse the operation safely, and some actions—sending an email, charging a card—cannot be undone in the ordinary sense.
### 15. Interpreter
Interpreter represents and evaluates a small language, grammar, or expression format.
Examples include:
- Simple filter expressions.
- A game’s tiny dialogue or quest-rule language.
- Arithmetic or logical expressions.
- A constrained user-defined query format.
For a full programming language or complex query system, use a mature parser or expression library rather than building an interpreter one `switch` statement at a time. Also validate input carefully if the expression comes from an untrusted user.
### 16. Iterator
Iterator provides a way to traverse a collection without exposing how it is stored. C# makes this pattern familiar through `IEnumerable<T>`, `IEnumerator<T>`, and `yield return`.
```csharp
public static IEnumerable<int> EvenNumbers(int limit)
{
for (var value = 0; value <= limit; value += 2)
yield return value;
}
``
The language and runtime handle much of the iterator machinery. Consider whether enumeration is deferred: code inside an iterator may execute when the caller enumerates, not when the sequence is created.
### 17. Mediator
Mediator centralizes communication between collaborating objects, reducing direct references between them.
A UI mediator might coordinate a form, validation status, and save action. A game event coordinator might route interactions among otherwise independent systems.
**Benefits:** components can be less tightly coupled.
**Drawbacks:** the mediator can become a central “everything knows everything” object. Keep its responsibilities bounded, or use focused events, message handlers, or application services instead.
### 18. Memento
Memento captures enough internal state to restore an object later without exposing every implementation detail to outside callers.
Useful cases include:
- Undo and redo.
- Save checkpoints.
- Restoring a game session.
- Rolling back an editor operation.
Snapshots can be expensive for large state graphs. Consider immutable data, compact deltas, or periodic snapshots plus event logs when full copies become costly. Ensure a saved snapshot has a clear versioning and compatibility strategy if it persists across app releases.
### 19. Observer
Observer lets one publisher notify multiple subscribers about an event or state change. C# events and event-handler patterns are common implementations.
In a game, a score change may notify the UI, audio system, and analytics component without the score model knowing each subscriber. In an app, a domain event can notify downstream handlers.
**Benefits:** publishers and subscribers can remain loosely coupled.
**Drawbacks:** forgotten subscriptions can cause memory leaks or callbacks after an object is disposed. In asynchronous systems, also define delivery guarantees, ordering, retries, and failure handling. For durable cross-process messaging, ordinary in-process events are not a substitute for a message broker.
### 20. State
State represents an object’s modes as separate behavior rather than scattering state-dependent logic everywhere.
A player might be `Grounded`, `Jumping`, or `Stunned`. A document might be `Draft`, `Submitted`, or `Approved`. Each state can define which transitions and operations are valid.
Start with an enum and a clear transition method if the behavior is small. Consider state classes when each state has substantial rules and the conditional logic keeps growing.
| Approach | Works well when | Warning sign |
|---|---|---|
| Enum plus `switch` | Few states and simple behavior | Same switch repeated across many classes |
| State objects | Many state-specific rules or transitions | Each state class is tiny boilerplate |
| State machine library | Complex, visual, or data-driven transitions | Library adds more machinery than the project needs |
### 21. Strategy
Strategy encapsulates interchangeable algorithms behind a common contract. It is a strong fit when the choice of behavior can vary at runtime or by configuration.
For an app, different shipping calculations can implement `IShippingCostStrategy`. For a game, enemy movement, targeting, or damage calculation can use different strategies.
```csharp
public interface ITargetingStrategy
{
Enemy? ChooseTarget(IReadOnlyList<Enemy> candidates);
}
public sealed class NearestTargetStrategy : ITargetingStrategy
{
public Enemy? ChooseTarget(IReadOnlyList<Enemy> candidates) =>
candidates
.OrderBy(enemy => enemy.DistanceFromPlayer)
.FirstOrDefault();
}
``
**Benefits:** new behaviors can be added without filling one class with conditionals. Strategies are easy to test independently.
**Drawbacks:** a class per tiny rule can become excessive. A delegate may be enough when the behavior is simple and has no independent state.
### 22. Template Method
Template Method defines the outline of an algorithm and lets subclasses customize selected steps.
For example, a report workflow may always load data, format content, and save output, while subclasses change the formatting step.
**Benefits:** shared sequence logic stays in one place.
**Drawbacks:** inheritance creates tighter coupling and can make combinations inflexible. If several steps vary independently, composition or Strategy may be a better fit.
### 23. Visitor
Visitor separates operations from the object structure they operate on. It can be useful when the set of element types is stable but new operations are added regularly—for instance, formatting or analysis across an expression tree.
The trade-off is real: adding a new element type can require updating each visitor. Visitor is therefore a targeted tool, not the default answer for every hierarchy.
## 🧪 Applying Patterns in Real C# and .NET Applications
Patterns pay their way when they clarify a real application boundary: a controller, gameplay system, data-access component, background worker, or integration with an external provider. The official [.NET architecture guidance](https://learn.microsoft.com/en-us/dotnet/architecture/) is a strong starting point for considering those boundaries.
### Design Patterns in ASP.NET Core
ASP.NET Core already uses and supports several familiar ideas:
- **Dependency injection** supplies services to controllers and other components.
- **Middleware** forms a request pipeline, similar to Chain of Responsibility.
- **Options** lets services receive configured settings.
- **Hosted services** provide a structure for background work.
- **Filters** allow cross-cuting behavior around MVC actions.
- **`IHttpClientFactory`** supports configurable HTTP clients and handler pipelines.
For example, a controller should usually delegate important work to an application service rather than create repositories, HTTP clients, and domain objects by hand. Keep controllers thin, but don’t turn every use case into a generic “manager” with no clear purpose. Explore more practical [back-end technology guidance](https://stackinterface.com/category/back-end-technologies/).
### Dependency Injection and Service Lifetimes
.NET’s built-in container supports three common lifetimes:
| Lifetime | Instance behavior | Typical use | Main caution |
|---|---|---|---|
| **Transient** | New instance each time requested | Lightweight, stateless services | Too much repeated setup can be wasteful |
| **Scoped** | One instance per scope, commonly one web request | Request-level work and scoped data access | Do not capture it inside a singleton |
| **Singleton** | One shared instance for the service registration | Thread-safe shared services and caches | Avoid mutable state unless concurrency is handled |
These lifetimes are documented in Microsoft’s [dependency-injection guidance](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection). A common bug is a **captive dependency**: a singleton holds onto a scoped service longer than intended. Let the container manage the object graph, and check lifetime compatibility.
Practical habits:
- Register services near the application’s composition root.
- Depend on small, meaningful interfaces where substitution is valuable.
- Avoid injecting the entire service provider as a service locator.
- Keep secrets out of source control; use [ASP.NET Core user secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) for local development and a managed secret store in production.
- Test important behavior using real implementations where practical, and use test doubles selectively.
### Patterns for Data Access, APIs, and Background Services
**Repository** is a common way to abstract data access, but it is not one of the 23 GoF patterns. It can help when it separates domain-facing operations from persistence details. It can hurt when it merely duplicates every method on Entity Framework Core’s `DbSet<T>` and hides useful query capabilities.
**Unit of Work** coordinates a set of changes that should be committed together. Entity Framework Core’s `DbContext` already tracks changes and saves them together with `SaveChanges` or `SaveChangesAsync`, so adding a generic Unit of Work wrapper may be redundant. Microsoft explains the role of [DbContext in EF Core](https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/).
For APIs and background jobs:
- Use **Adapter** at an external-provider boundary.
- Use **Decorator** or middleware for cross-cuting concerns such as logging and retries.
- Use **Command** when work needs queuing, scheduling, or audit records.
- Use **Observer or messaging** for notifications, choosing durable infrastructure when delivery must survive process restarts.
- Keep retry behavior idempotent where possible; repeating a payment order operation must not accidentally duplicate side effects.
### Async, Thread Safety, and Performance Considerations
Patterns do not make code fast by default. Measure before optimizing, then apply the smallest change that addresses the measured bottleneck.
For async .NET code:
- Prefer `async`/`await` for I/O-bound workflows.
- Pass `CancellationToken` through layers that can honor cancellation.
- Avoid blocking asynchronous work with `.Result` or `.Wait()` in request paths.
- Make shared singleton services thread-safe.
- Avoid unnecessary allocations in frequently executed game loops or real-time paths.
For games, profile in the target engine and device. Unity provides [Profiler documentation](https://docs.unity3d.com/Manual/Profiler.html); its [Garbage Collection guidance](https://docs.unity3d.com/Manual/performance-garbage-collection-best-practices.html) discusses ways to reduce allocation pressure. Object Pooling can help with frequently reused objects such as projectiles, but pooling adds lifecycle complexity. It is not a substitute for measuring.
For other performance-focused engineering topics, see Stack Interface’s [Coding Best Practices](https://stackinterface.com/category/coding-best-practices/) and [Data Science](https://stackinterface.com/category/data-science/) resources.
## ⚖️ How to Choose Between Common C# Patterns
When two patterns sound similar, compare the **problem they solve**, not just the class diagrams. Here’s the short version.
| Comparison | Choose the first when… | Choose the second when… |
|---|---|---|
| Factory vs. Builder | You need to choose which product to create | You need to assemble one complex product step by step |
| Strategy vs. State | You need to swap an algorithm | An object’s mode determines valid behavior and transitions |
| Decorator vs. Proxy | You want to add composable behavior | You want to control or mediate access to another object |
| Adapter vs. Facade | You need to translate one interface to another | You want a simpler entry point to a subsystem |
| Observer vs. Mediator | A publisher notifies independent subscribers | A coordinator manages interactions among components |
### Factory vs. Builder
A **Factory** answers, “Which object should I create?” A **Builder** answers, “How do I assemble this complex object?”
Use a factory when a format, platform, or configuration chooses an implementation. Use a builder when a product needs several construction steps or validations. Some systems use both, but don’t combine them just to make the object diagram look impressively busy.
### Strategy vs. State
**Strategy** selects among interchangeable behaviors, such as pricing algorithms or enemy targeting rules. **State** represents an object’s current mode and often controls its allowed transitions, such as `Open`, `Closed`, or `Locked`.
Ask: “Is this a choice of algorithm for a task, or a lifecycle mode that changes what the object can do?” If it’s the latter, State may fit better.
### Decorator vs. Proxy
Both wrap another object. A **Decorator** adds behavior while preserving the same interface, often in composable layers. A **Proxy** primarily controls access to the wrapped object, such as through lazy loading, authorization, or remote calls.
The distinction can blur in real code. Choose the description that best communicates the wrapper’s purpose to your team.
### Adapter vs. Facade
An **Adapter** changes an interface so two components can work together. A **Facade** simplifies access to a subsystem. An adapter speaks the caller’s language; a facade gives the caller fewer things to learn.
### Observer vs. Mediator
**Observer** is about publishing changes to subscribers. **Mediator** is about coordinating interactions among colleagues through a central component. A mediator may use events internally, but the design goal is different.
Use Observer when independent listeners should react. Use Mediator when direct collaboration among many components is creating tangled dependencies.
## 🚫 Common Design Pattern Mistakes to Avoid
Patterns are useful until they stop being useful. Watch for these common traps:
- **Pattern hunting:** selecting a pattern before identifying the problem.
- **Premature abstraction:** building for hypothetical implementations no one needs.
- **Overusing interfaces:** an interface is useful when it represents a meaningful contract or boundary, not merely because a class exists.
- **Calling every helper a Factory:** a method that constructs one object is not automatically a full GoF Factory Method.
- **Treating Singleton as harmless global state:** shared mutable data creates concurrency and testing risks.
- **Duplicating framework features:** a custom pipeline or service container may be unnecessary when .NET already provides one.
- **Overengineering repositories:** wrappers that simply mirror EF Core APIs can hide rather than clarify.
- **Ignoring runtime costs:** virtual calls, allocations, reflection, and indirection can matter in hot paths; profile first.
- **Creating a god object:** a Facade or Mediator should not become the place where all application behavior accumulates.
- **Forgetting lifecycle and cleanup:** event subscriptions, pooled objects, background tasks, and disposable resources need explicit ownership.
- **Applying inheritance where composition is clearer:** Template Method and some factory designs can lock behavior into rigid hierarchies.
- **Confusing an in-process event with durable messaging:** an event disappears with the process unless a persistent delivery mechanism backs it.
A useful review question is: **Could a teammate explain why this pattern exists in one sentence?** If not, simplify or document the actual design pressure.
## 🛠️ Testing, Refactoring, and Maintaining Pattern-Based Code
A pattern should make change safer, and tests help verify that claim. You do not need to unit-test every trivial forwarding method, but you should test meaningful behavior and the boundaries where mistakes are costly.
### A practical testing workflow
1. **Test the behavior through its contract.**
A Strategy implementation can be tested through the expected interface; an adapter can be tested against the vendor API’s behavior.
2. **Keep tests focused.**
Test the shipping rule, not every class in the application just to prove the container can instantiate it.
3. **Use fakes and mocks deliberately.**
Replace slow or external dependencies when isolation helps, but don’t mock so much that a test verifies only its own setup.
4. **Test failure paths and transitions.**
For State, test invalid transitions. For a Chain of Responsibility, test what happens when no handler accepts the request.
5. **Add integration tests at real boundaries.**
Verify database mappings, HTTP contracts, and dependency injection configuration where those details matter.
6. **Measure before and after performance changes.**
Keep benchmarks representative of real workloads and target devices.
Microsoft’s [unit testing guidance for .NET](https://learn.microsoft.com/en-us/dotnet/core/testing/) provides a starting point. For game projects, also test behavior in the engine and on representative hardware; a design that is tidy in a desktop test may still allocate too much in a mobile frame loop.
### Refactor toward a pattern when the pressure appears
Avoid implementing a pattern simply because the roadmap might someday require it. Instead:
- Start with straightforward code.
- Notice the repeated variation or coupling.
- Add the smallest abstraction that removes the pain.
- Write tests around the behavior that must stay stable.
- Revisit the design when a second real implementation appears.
This approach preserves clarity now without making future change unnecessarily expensive.
## ✅ C# Design Patterns Quick Reference
The table below summarizes the **23 GoF patterns** and the design pressure each addresses. This is a map, not a prescription.
| Category | Pattern | Use it when… |
|---|---|---|
| Creational | Factory Method | The creation method or subclass should decide which product to return |
| Creational | Abstract Factory | You need compatible families of related products |
| Creational | Builder | Construction has meaningful steps, options, or validation |
| Creational | Prototype | A configured instance should be copied under a defined copy policy |
| Creational | Singleton | One shared instance is genuinely required and safely managed |
| Structural | Adapter | Existing interfaces do not match |
| Structural | Bridge | Abstraction and implementation need to vary independently |
| Structural | Composite | Individual objects and groups should be treated uniformly |
| Structural | Decorator | Behavior should be added around an object in composable layers |
| Structural | Facade | Callers need a simpler interface to a subsystem |
| Structural | Flyweight | Measured memory pressure can be reduced by sharing common state |
| Structural | Proxy | Access to another object needs mediation |
| Behavioral | Chain of Responsibility | A request should pass through a set of possible handlers |
| Behavioral | Command | An action needs to be represented, queued, logged, or replayed |
| Behavioral | Interpreter | A small language or grammar needs to be evaluated |
| Behavioral | Iterator | Callers need to traverse without knowing collection internals |
| Behavioral | Mediator | A coordinator can reduce direct links among collaborators |
| Behavioral | Memento | State must be captured and restored |
| Behavioral | Observer | Multiple subscribers should react to publisher notifications |
| Behavioral | State | Behavior and valid actions depend on an object’s current state |
| Behavioral | Strategy | Algorithms should be interchangeable |
| Behavioral | Template Method | Subclasses vary selected steps within a fixed workflow |
| Behavioral | Visitor | New operations must be added across a relatively stable element structure |
### Patterns you’ll commonly meet in .NET projects
| Pattern or related idea | Common .NET expression |
|---|---|
| Dependency Injection | `IServiceCollection` registrations and constructor injection |
| Iterator | `IEnumerable<T>`, `IEnumerator<T>`, `yield return` |
| Observer | Events, event handlers, and messaging abstractions |
| Decorator | Wrapped service implementations or HTTP handler pipelines |
| Chain of Responsibility | ASP.NET Core middleware pipeline |
| Repository | Application-defined data-access abstraction, when it adds value |
| Unit of Work | Coordinated persistence changes; often already represented by EF Core `DbContext` |
These additional practices are common but **are not all members of the 23-pattern GoF catalog**. That distinction resolves a frequent counting question: there are 23 classic GoF patterns, but C# developers use many other patterns and architectural approaches as well.




