Design Pattern
# Design Patterns in C#: Examples, OOP, and SOLID
Design patterns are reusable approaches to recurring software design problems. This repository demonstrates 18 creational, structural, and behavioral patterns with small C# examples. Each guide below describes the pattern, shows how to use it, explains when it is useful, and maps it to object-oriented programming (OOP) and SOLID principles.
## How OOP and SOLID fit together
OOP principles describe how objects hide state, expose abstractions, collaborate through composition, and use inheritance or polymorphism. SOLID builds on those ideas as design guidance: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion help keep object-oriented designs understandable and adaptable. A pattern may support some principles directly; it does not automatically satisfy all five.
## Creational patterns
### Builder
## Usage
Builder constructs a complex object step by step. The computer example lets callers specify required parts and optional features, then validates and creates the computer with `Build()`.
## When to use it
Use Builder when an object has many configuration options, especially when some are optional or construction needs validation. It makes object creation readable without a long list of constructor arguments.
### Relationship between SOLID and OOP principles
`ComputerBuilder` encapsulates incremental construction and validation, then returns a `Computer`. Fluent methods return the builder for a readable creation protocol; the client depends on the product rather than managing partially initialized state.
- **SRP:** The builder owns construction rules, while `Computer` represents the finished product.
- **OCP:** New construction options can be added to the builder, but changing the product schema may still require coordinated changes.
- **DIP:** The sample client directly creates `ComputerBuilder`; a system with multiple construction processes could depend on a builder abstraction.
- **ISP:** A focused builder API exposes only meaningful construction steps.
- **LSP:** If multiple builders share an abstraction, each must produce a valid product under its contract.
Builder improves object creation but is not automatically an application of every SOLID rule; avoid adding a director or interface when one simple builder is enough.
### Prototype
## Usage
Prototype creates a new object by copying an existing one. The document example clones both the document and its section list, so edits to the draft do not change the original.
## When to use it
Use Prototype when creating an object from scratch is costly or complicated, or when new instances should begin as copies of a configured template. Choose shallow or deep copying deliberately based on whether referenced data should be shared.
### Relationship between SOLID and OOP principles
The configured `Document` acts as its own prototype through `Clone()`. Clients create variants by asking the object to copy itself, without depending on a separate concrete factory or constructor recipe. The example copies the mutable section list so edits remain independent.
- **SRP:** Cloning is part of the document’s creation responsibility; a dedicated copier may be preferable if copy rules become complex.
- **OCP:** New document variants can be cloned through the same convention without a central type switch.
- **DIP:** A larger design can depend on an `IPrototype<T>` abstraction; this small sample uses the concrete document.
- **LSP:** Every prototype implementation must produce a valid independent copy with clearly defined shared references.
- **ISP:** A single clone operation is a narrow contract when callers only need duplication.
Prototype correctness depends on copy semantics. Nested mutable objects require deeper copying or explicit sharing rules.
### Singleton
## Usage
Singleton provides one shared instance through a public access point. `AppSettings` uses `Lazy<T>` so the instance is created once, on first access, and safely shared across threads.
## When to use it
Use Singleton only when the application truly requires one shared instance, such as a process-wide configuration provider. Consider dependency injection instead when it can provide the required lifetime more clearly; global state can make code harder to test and reason about.
### Relationship between SOLID and OOP principles
The private constructor and static `Instance` property control instantiation and expose one shared object. This uses encapsulation, but consumers couple themselves to the global `AppSettings` access point.
- **SRP:** The class is responsible both for settings data and for controlling its own lifetime; these concerns can be separated if they grow.
- **DIP:** Direct static access is a dependency on a concrete global, so it commonly works against DIP. Injecting an `IAppSettings` abstraction is easier to test.
- **OCP / LSP / ISP:** These are not inherent Singleton benefits. An interface around settings can support substitutability, but the pattern itself does not ensure it.
Use this pattern only when single-instance semantics are a real requirement. A singleton is a lifetime choice, not proof that a class is well-designed.
## Structural patterns
### Adapter
## Usage
Adapter converts one interface into another expected by the client. `PaymentAdapter` wraps the legacy gateway and exposes its API as `IModernPayment`.
## When to use it
Use Adapter when integrating existing or third-party code whose interface does not match the one your application expects.
### Relationship between SOLID and OOP principles
`PaymentAdapter` implements the target `IModernPayment` contract and composes a `LegacyPaymentGateway`, translating the call and amount representation. The client talks to the target abstraction rather than the incompatible adaptee.
- **SRP:** The adapter owns translation; the legacy gateway retains its original responsibility.
- **OCP:** Integrate another legacy provider with another adapter, leaving clients unchanged.
- **DIP:** Client code depends on `IModernPayment`; inject that abstraction to avoid constructing the adapter at the use site.
- **LSP:** The adapter must preserve the target contract’s expected payment behavior, including errors and amount precision.
- **ISP:** A narrow target interface means the adapter need only support the operation clients use.
Conversion between `decimal` and `double` in this demo is illustrative; production adapters must preserve domain correctness and error semantics.
### Bridge
## Usage
Bridge separates an abstraction from its implementation so both can vary independently. Here, a `Remote` works with any `IDevice`, including a television or radio.
## When to use it
Use Bridge when two dimensions of a design need to evolve independently, such as controls and devices or reports and output formats. It prevents a subclass for every combination.
### Relationship between SOLID and OOP principles
`Remote` is the abstraction and holds an `IDevice` implementation. This composition-based relationship lets a remote and its device implementations vary independently; `Television` and `Radio` are interchangeable through polymorphism.
- **SRP:** Remote controls command behavior; devices implement device-specific channel changes.
- **OCP:** Add devices or richer remote types without creating every combination as a subclass.
- **DIP:** `Remote` depends on `IDevice`, not a television or radio class.
- **LSP:** Every device must honor the channel-setting contract expected by remotes.
- **ISP:** Keep `IDevice` limited to capabilities the abstraction uses; split broader device APIs when needed.
Bridge’s benefit comes from having two real axes of variation. For only one or two fixed implementations, this extra abstraction may not pay off.
### Composite
## Usage
Composite gives individual objects and groups a shared interface. The file system example lets a folder contain files and other folders, then displays the tree recursively.
## When to use it
Use Composite when clients should treat a single item and a nested group uniformly, such as files and folders, menu trees, or UI components.
### Relationship between SOLID and OOP principles
`FileItem` and `FolderItem` implement `IFileSystemItem`; folders compose other components and recursively delegate `Display`. Clients use the same abstraction for leaves and composites, demonstrating composition and polymorphism.
- **SRP:** Each component renders itself; a folder manages its children and recursive traversal.
- **OCP:** Add new item types behind `IFileSystemItem` without changing traversal clients.
- **LSP:** A leaf and a folder can be used wherever the shared display contract is expected. Avoid putting child-management methods on the common interface if leaves cannot meaningfully support them.
- **ISP:** The example’s display-only interface is narrow; keeping `Add` on folders avoids forcing it on files.
- **DIP:** Callers depend on `IFileSystemItem`, though the demo builds concrete objects directly.
Uniform treatment is most valuable when clients need recursive operations; it may conceal capability differences if the common interface grows too broad.
### Decorator
## Usage
Decorator wraps an object with another object that implements the same interface, adding behavior without changing the original class. The coffee example adds milk and its cost around a base coffee.
## When to use it
Use Decorator when optional features should be combinable at runtime and subclass combinations would become unwieldy.
### Relationship between SOLID and OOP principles
`MilkDecorator` implements `ICoffee` and wraps another `ICoffee`, forwarding and extending its description and cost. Composition lets decorators stack while polymorphism keeps the client unaware of wrapper layers.
- **SRP:** Each decorator adds one responsibility or feature.
- **OCP:** Add a new decorator without modifying `HouseCoffee` or existing decorators.
- **DIP:** The wrapper depends on the `ICoffee` abstraction, not a particular coffee class.
- **LSP:** A decorated coffee remains usable as `ICoffee` and must preserve meaningful cost/description semantics.
- **ISP:** The narrow component interface makes consistent decoration possible.
Decorator is flexible, but stacked wrappers can make object identity, ordering, and debugging less obvious. Keep each wrapper’s contract clear.
### Facade
## Usage
Facade offers a simple entry point to a group of services. `OrderFacade` coordinates inventory reservation, payment, and shipping behind one operation.
## When to use it
Use Facade when clients need a simpler interface to a complex subsystem or when you want to reduce direct dependencies on several subsystem classes.
### Relationship between SOLID and OOP principles
`OrderFacade` composes inventory, payment, and shipping services and exposes one high-level `PlaceOrder` operation. It hides coordination details; it does not replace the subsystem objects or prevent other clients from using them.
- **SRP:** The facade coordinates an order workflow, while each service performs its own subsystem task. Keep unrelated workflows out of the same facade.
- **DIP:** This sample constructs concrete services internally, which couples the facade to them; constructor-injected interfaces would improve substitution and testing.
- **OCP:** New subsystem steps may require editing the facade workflow, so OCP is conditional rather than automatic.
- **ISP:** A small facade operation gives clients a focused API.
- **LSP:** Relevant if facades implement a shared application contract; alternate implementations must preserve that contract.
Facade primarily simplifies coupling at the client boundary; it is not itself a guarantee of dependency inversion or extensibility.
### Flyweight
## Usage
Flyweight shares immutable data across many objects and keeps per-instance data outside the shared object. The tree example shares each tree type’s name and color while each tree keeps its own coordinates.
## When to use it
Use Flyweight when an application creates many similar objects and profiling shows that repeated shared state consumes significant memory.
### Relationship between SOLID and OOP principles
`TreeFactory` shares `TreeType` objects keyed by intrinsic state (name and color); coordinates are supplied per use as extrinsic state. This separates shared immutable data from per-tree context and uses a factory to control reuse.
Get Ibrahim Maher’s stories in your inbox
Join Medium for free to get updates from this writer.
- **SRP:** `TreeType` holds shared type data; the factory manages lookup and reuse; callers provide position.
- **OCP:** New tree types are supplied as data, though keying rules or storage changes belong to the factory.
- **DIP:** Clients currently call the static concrete factory; an injected factory interface would allow alternate stores or tests.
- **LSP / ISP:** These principles are not central unless flyweights sit behind a shared interface; any such contract should be small and consistently implemented.
Shared flyweights should be immutable or safely synchronized. This sample’s mutable static dictionary is not thread-safe, and memory savings should be measured before adding this complexity.
### Proxy
## Usage
Proxy implements the same interface as a real object and controls access to it. The protected image proxy checks permission before loading and displaying the image.
## When to use it
Use Proxy when access to an object needs control or extra handling, such as authorization, lazy loading, caching, logging, or remote access.
### Relationship between SOLID and OOP principles
`ProtectedImageProxy` and `RealImage` implement `IImage`. The proxy controls whether a request reaches the real object while clients keep using the common abstraction; this is composition and polymorphism at an access boundary.
- **SRP:** The proxy owns access control; the real image owns display behavior. More proxy concerns can be composed or split as they grow.
- **OCP:** Introduce a proxy for caching or lazy loading without changing clients or the real image.
- **DIP:** Clients should depend on `IImage`; the sample still constructs concrete proxies directly.
- **LSP:** A proxy must preserve expected `IImage` semantics, including return values, errors, and timing where those matter.
- **ISP:** The small display interface keeps both proxy and real object focused.
This demo checks a fixed Boolean and constructs the real image during display; a production proxy would commonly retain one real instance and use an authorization service abstraction.
## Behavioral patterns
### Iterator
## Usage
Iterator provides a way to visit items in a collection one at a time without exposing how that collection stores them. In this example, an `EmployeeIterator` traverses the employees held by an HR collection.
## When to use it
Use Iterator when callers need a consistent way to traverse a collection, or when you want to support multiple traversal strategies without exposing the collection’s internal representation.
### Relationship between SOLID and OOP principles
`IEmployeeCollection` and `Iterator<T>` are abstractions; `HRDepartment` and `EmployeeIterator` provide concrete collection and traversal behavior. The client depends on the iterator contract rather than the list representation (encapsulation, abstraction, and polymorphism).
- **SRP:** The collection stores employees, while the iterator tracks traversal position. This separation is clearest when each iterator owns its cursor.
- **OCP:** New collection types or traversal orders can be added behind the interfaces without changing clients.
- **DIP:** Clients should receive/use `Iterator<Employee>` rather than depend on `EmployeeIterator` or the department’s internal `List<Employee>`.
- **LSP / ISP:** Implementations must honor the iterator’s traversal contract; keep iterator and collection interfaces small and focused.
Iterator does not automatically guarantee all SOLID principles: a concrete iterator that exposes storage or has surprising `Next()` behavior weakens substitutability and encapsulation.
### Mediator
## Usage
Mediator routes requests between colleagues so they do not need direct references to one another. This project uses requests and handlers to keep order operations separate from the mediator that dispatches them.
## When to use it
Use Mediator when many objects communicate with one another and their dependencies are becoming tangled. It centralizes coordination while keeping each participant focused on its own work.
### Relationship between SOLID and OOP principles
Requests (`IRequest<T>`) and handlers (`IRequestHandler<,>`) model roles as objects; `IMediator` hides handler lookup and dispatch. The example uses encapsulation and polymorphism to route commands and queries to distinct handlers.
- **SRP:** Each handler owns one request’s use case; the mediator owns dispatch. Keep persistence and business rules out of the mediator.
- **OCP:** Add request/handler pairs without changing callers. The simple mediator’s registration mechanism is the extension point.
- **DIP:** Callers depend on `IMediator`, and handlers can depend on repository abstractions in a fuller design. This demo currently constructs concrete classes, so it illustrates the boundary more than full dependency injection.
- **ISP:** Separate request-specific handlers keep each contract narrow.
- **LSP:** Any registered handler must fulfill the request’s response and behavior contract.
Mediator can become a god object if it accumulates business logic; SOLID depends on keeping it as a coordinator.
### Memento
## Usage
Memento captures an object’s state so it can be restored later without exposing the object’s internals. The text editor example saves snapshots and lets a history manager restore them.
```csharp
history.Save(editor.CreateSnapshot());
editor.Type(“ more text”);
editor.Restore(history.Undo());
```
## When to use it
Use Memento for undo/redo, checkpoints, or rollback when an object’s state must be saved and restored while its encapsulation remains intact.
### Relationship between SOLID and OOP principles
`TextEditor` is the originator that creates and restores `Text` snapshots; `HistoryManager` is the caretaker holding snapshots without interpreting the editor’s state. This uses encapsulation and object composition to keep state management outside the editor.
- **SRP:** The editor edits text; history tracks undo/redo snapshots.
- **OCP:** New caretaker policies (bounded history, persistence) can be added without changing the editor’s editing API, ideally behind a history abstraction.
- **DIP:** The current caretaker uses concrete `Text` and `TextEditor`; interfaces for snapshot storage/originators would decouple a larger system.
- **ISP:** A narrow snapshot API should expose only capture/restore operations needed by its collaborators.
- **LSP:** Alternative snapshots must preserve the same restore semantics.
Memento mainly supports SRP and encapsulation; it does not require an elaborate class hierarchy. Snapshot copying must also be deep enough for mutable state.
### Observer
## Usage
Observer lets subscribers register for events from a publisher. The marketplace example publishes event types such as a new product or price drop to subscribed customers and sellers.
## When to use it
Use Observer when one change should notify an unknown or changing set of listeners, such as UI events, notifications, or domain events. It keeps the publisher independent of concrete subscribers.
### Relationship between SOLID and OOP principles
`MarketplaceEventManager` holds subscriber abstractions (`ISubscriber`) and sends notifications polymorphically. Customers and sellers can respond differently to the same event without the publisher knowing their concrete types.
- **SRP:** The event manager manages subscriptions and publication; each subscriber handles its own response.
- **OCP:** Add subscriber types or event reactions without editing publisher logic.
- **DIP:** The publisher depends on `ISubscriber`, not customer/seller classes.
- **ISP:** A small notification contract limits what subscribers must implement.
- **LSP:** Every subscriber implementation should safely accept notifications under the shared contract.
This decoupling is useful, but synchronous observers can still create hidden ordering, failure, and lifetime concerns; those policies belong in the design when relevant.
### State
## Usage
State represents an object’s current condition as a separate state object. The order context delegates actions to states such as Created, Paid, Shipped, Delivered, or Cancelled.
## When to use it
Use State when behavior depends on an object’s current state and conditional logic is growing. Each state can define valid actions and transitions for that condition.
### Relationship between SOLID and OOP principles
`OrderContext` delegates operations to an `IOrderState`; concrete state objects encapsulate state-specific behavior and transitions. Runtime polymorphism replaces repeated state conditionals in the context.
- **SRP:** Each state class owns the rules for one order condition; the context owns the current-state reference.
- **OCP:** Add a new state implementation with localized changes, though transitions may require updates in multiple states.
- **DIP:** The context should depend on `IOrderState`, not concrete states.
- **LSP:** Each state must honor the context’s operation contract; invalid transitions should have deliberate, consistent behavior.
- **ISP:** Keep state operations cohesive; split contracts only if states genuinely support different capabilities.
State objects make behavior explicit but can add classes. They are most useful when transitions and state-dependent rules have real complexity.
### Strategy
## Usage
Strategy packages interchangeable algorithms behind a shared interface. This example selects pricing and checkout strategies, then passes them to products or a checkout processor.
## When to use it
Use Strategy when an operation has several valid algorithms and the choice may vary by context or at runtime. It avoids large conditionals and makes each algorithm independently replaceable.
### Relationship between SOLID and OOP principles
Pricing and checkout strategies share interfaces (`IPricingStrategy`, `ICheckoutStrategy`) while implementing different algorithms. Products/processors use composition to delegate work; the selected implementation can vary without changing the client.
- **SRP:** Each strategy contains one algorithm; the context applies it.
- **OCP:** Add a strategy without changing existing algorithms or the context, provided selection is also extensible.
- **DIP:** Contexts depend on strategy interfaces rather than concrete payment/pricing classes.
- **LSP:** Strategies must obey common input/output and side-effect expectations so they can be substituted safely.
- **ISP:** Separate pricing and checkout contracts avoid forcing one strategy to implement unrelated operations.
The factory/manager in the example may still require edits for each new strategy; a strategy interface alone does not make selection open for extension.
### Template
## Usage
Template Method defines the fixed steps of an algorithm in a base class and lets subclasses customize selected steps. The CV report generator shares its workflow while PDF, Excel, and photo report generators provide format-specific behavior.
## When to use it
Use Template Method when several implementations follow the same overall process but differ in a few steps. It keeps the process consistent and avoids duplicating the shared workflow.
### Relationship between SOLID and OOP principles
`CVReportGenerator` defines the invariant workflow and dispatches customizable steps through abstract/virtual methods. Concrete generators override parsing and section generation, using inheritance and dynamic dispatch.
- **SRP:** The base class owns orchestration; subclasses own format-specific steps.
- **OCP:** New formats can extend the base workflow without rewriting its sequence.
- **LSP:** Subclasses must preserve the promised workflow and step assumptions; overrides that skip required behavior break substitutability.
- **DIP:** The demo selects concrete subclasses directly. A higher-level caller can depend on the base abstraction or a generator interface.
- **ISP:** Not every SOLID benefit is central here; keep customization hooks limited to steps that truly vary.
Template Method relies on inheritance, so changes to the base algorithm affect every subclass. Prefer Strategy when variation needs runtime composition or independent combinations.
### Visitor
## Usage
Visitor separates operations from the object structure they operate on. The file and folder elements accept visitors such as size calculation or report generation.
## When to use it
Use Visitor when you need to add several operations to a relatively stable set of element types. It keeps those operations out of the element classes; adding new element types can require updates to visitors.
### Relationship between SOLID and OOP principles
Elements expose `Accept(IVisitor)` and dispatch themselves to type-specific visitor methods. `SizeVisitor` and `ReportVisitor` implement operations polymorphically, separating operations from the file/folder structure.
- **SRP:** Elements model structure; each visitor owns one operation such as size aggregation or reporting.
- **OCP:** Add a new operation by adding a visitor without changing element classes.
- **DIP:** Elements and operations meet through `IVisitor`/`IElement` abstractions, though concrete `Visit(File)` and `Visit(Folder)` signatures couple the visitor to the element set.
- **ISP:** The visitor contract includes one method per element kind; it stays manageable for a small stable set but grows with element types.
- **LSP:** Element implementations must consistently accept visitors and expose the promised structure.
Visitor favors adding operations over adding element types: a new element type usually requires changing every visitor, so it is a poor fit when the structure changes frequently.
## Explore the examples
The project targets .NET 9 and provides a console menu for running each example. See the source repository for the runnable code and per-pattern guides: [DesignPatterns on GitHub]



